diff --git a/README.md b/README.md index 03c753cce..35ae7f371 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,21 @@ OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many ma -cookie-secure=false -email-domain example.com +### Slack Provider + +Register a new Slack App to get a ClientID and Client Secret: + +1. Go to your applications at: https://api.slack.com/applications +2. "Create a new App" or choose an existing one +3. In the application settings, go to "OAuth & Permissions" and add a new redirect URL, e.g. `https://internal.yourcompany.com/oauth2/callback` +4. Get the Client ID and Client Secret from the "Basic Information" page + +Authentication can be restricted to a single team with the `-slack-team` command line or the `slack_team` settings entry. Both require your team's Team ID. + +You can get it by creating a token for your workspace: https://api.slack.com/custom-integrations/legacy-tokens + +With this token "Test" the auth.test method here: https://api.slack.com/methods/auth.test/test The response contains the Team ID. + ## Email Authentication To authorize by email domain use `--email-domain=yourcompany.com`. To authorize individual email addresses use `--authenticated-emails-file=/path/to/file` with one email per line. To authorize all email addresses use `--email-domain=*`. diff --git a/main.go b/main.go index 287dc4894..9abb75874 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,7 @@ func main() { flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") flagSet.String("github-org", "", "restrict logins to members of this organisation") flagSet.String("github-team", "", "restrict logins to members of this team") + flagSet.String("slack-team", "", "restrict logins to members of this team") flagSet.Var(&googleGroups, "google-group", "restrict logins to members of this google group (may be given multiple times).") flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") flagSet.String("google-service-account-json", "", "the path to the service account json credentials") diff --git a/options.go b/options.go index 949fbba80..800638e16 100644 --- a/options.go +++ b/options.go @@ -34,6 +34,7 @@ type Options struct { EmailDomains []string `flag:"email-domain" cfg:"email_domains"` GitHubOrg string `flag:"github-org" cfg:"github_org"` GitHubTeam string `flag:"github-team" cfg:"github_team"` + SlackTeam string `flag:"slack-team" cfg:"slack_team"` GoogleGroups []string `flag:"google-group" cfg:"google_group"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"` @@ -278,6 +279,8 @@ func parseProviderInfo(o *Options, msgs []string) []string { } else { p.Verifier = o.oidcVerifier } + case *providers.SlackProvider: + p.SetTeamID(o.SlackTeam) } return msgs } diff --git a/providers/providers.go b/providers/providers.go index 70e707b43..7936d97a6 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -31,6 +31,8 @@ func New(provider string, p *ProviderData) Provider { return NewGitLabProvider(p) case "oidc": return NewOIDCProvider(p) + case "slack": + return NewSlackProvider(p) default: return NewGoogleProvider(p) } diff --git a/providers/slack.go b/providers/slack.go new file mode 100644 index 000000000..a06440adb --- /dev/null +++ b/providers/slack.go @@ -0,0 +1,130 @@ +package providers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" +) + +type SlackProvider struct { + *ProviderData + TeamID string +} + +// Slack API Response for https://api.slack.com/methods/users.identity +type SlackUserIdentityResponse struct { + OK bool + User SlackUserItem + Team SlackUserItem +} + +type SlackUserItem struct { + ID string + Name string + Email string +} + +func NewSlackProvider(p *ProviderData) *SlackProvider { + p.ProviderName = "slack" + if p.LoginURL == nil || p.LoginURL.String() == "" { + p.LoginURL = &url.URL{ + Scheme: "https", + Host: "slack.com", + Path: "/oauth/authorize", + } + } + if p.RedeemURL == nil || p.RedeemURL.String() == "" { + p.RedeemURL = &url.URL{ + Scheme: "https", + Host: "slack.com", + Path: "/api/oauth.access", + } + } + if p.ValidateURL == nil || p.ValidateURL.String() == "" { + p.ValidateURL = &url.URL{ + Scheme: "https", + Host: "slack.com", + Path: "/api", + } + } + if p.Scope == "" { + p.Scope = "identity.basic identity.email" + } + return &SlackProvider{ProviderData: p} +} + +func (p *SlackProvider) SetTeamID(team string) { + p.TeamID = team + // If a team id is set we can restrict login to this team directly at login + params, _ := url.ParseQuery(p.LoginURL.RawQuery) + params.Set("team", team) + p.LoginURL.RawQuery = params.Encode() +} + +func (p *SlackProvider) getIdentity(accessToken string) (*SlackUserIdentityResponse, error) { + params := url.Values{ + "token": {accessToken}, + } + endpoint := &url.URL{ + Scheme: p.ValidateURL.Scheme, + Host: p.ValidateURL.Host, + Path: path.Join(p.ValidateURL.Path, "/users.identity"), + RawQuery: params.Encode(), + } + req, _ := http.NewRequest("GET", endpoint.String(), nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf( + "got %d from %q %s", resp.StatusCode, endpoint.String(), body) + } + + var userIdentity SlackUserIdentityResponse + if err := json.Unmarshal(body, &userIdentity); err != nil { + return nil, err + } + + if userIdentity.OK == true { + return &userIdentity, nil + } + return nil, fmt.Errorf("slack response is not ok: %v", userIdentity) +} + +func (p *SlackProvider) hasTeamID(resp *SlackUserIdentityResponse) (bool, error) { + if resp.Team.ID != "" { + return resp.Team.ID == p.TeamID, nil + } + + return false, fmt.Errorf("no team id found") +} + +func (p *SlackProvider) GetEmailAddress(s *SessionState) (string, error) { + userIdentity, err := p.getIdentity(s.AccessToken) + if err != nil { + return "", nil + } + + // if we require a TeamId, check that first + if p.TeamID != "" { + if ok, err := p.hasTeamID(userIdentity); err != nil || !ok { + return "", err + } + } + + if email := userIdentity.User.Email; email != "" { + return email, nil + } + + return "", nil +}