Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Add Slack Provider #608

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=*`.
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
130 changes: 130 additions & 0 deletions providers/slack.go
Original file line number Diff line number Diff line change
@@ -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
}