Skip to content

Commit

Permalink
feat: add support for Slack OAuth V2 (#1591)
Browse files Browse the repository at this point in the history
## What kind of change does this PR introduce?

- Updates the Slack OAuth provider with the new Sign In With Slack V2.
- Creates a test for Slack, improving test coverage
- Moves the old Slack provider to slack_legacy. Some users might still
rely on this provider after the creation of legacy apps is disallowed on
June 4th.

## What is the current behavior?

Fixes #1294

Current behavior uses the original Slack OAuth V1 which is sunsetting
June 4th according to [the
changelog](https://api.slack.com/changelog/2024-04-discontinuing-new-creation-of-classic-slack-apps-and-custom-bots)

## What is the new behavior?

New behavior now leverages the new [Sign In With
Slack](https://api.slack.com/authentication/sign-in-with-slack) (SIWS)
on OAuth V2 for Slack authentication.

## Additional context

A ticket should be created for ending support on slack_legacy.

---------

Co-authored-by: Kang Ming <[email protected]>
  • Loading branch information
zhawtof and kangmingtay committed Jun 12, 2024
1 parent cd7b191 commit bb99251
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 2 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ To see the current settings, make a request to `http://localhost:9999/settings`
"facebook": false,
"spotify": false,
"slack": false,
"slack_oidc": false,
"twitch": true,
"twitter": false,
"email": true,
Expand Down
8 changes: 8 additions & 0 deletions hack/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ GOTRUE_EXTERNAL_LINKEDIN_ENABLED=true
GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_LINKEDIN_SECRET=testsecret
GOTRUE_EXTERNAL_LINKEDIN_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_LINKEDIN_OIDC_ENABLED=true
GOTRUE_EXTERNAL_LINKEDIN_OIDC_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_LINKEDIN_OIDC_SECRET=testsecret
GOTRUE_EXTERNAL_LINKEDIN_OIDC_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_GITLAB_ENABLED=true
GOTRUE_EXTERNAL_GITLAB_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_GITLAB_SECRET=testsecret
Expand All @@ -80,6 +84,10 @@ GOTRUE_EXTERNAL_SLACK_ENABLED=true
GOTRUE_EXTERNAL_SLACK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SLACK_SECRET=testsecret
GOTRUE_EXTERNAL_SLACK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_SLACK_OIDC_ENABLED=true
GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SLACK_OIDC_SECRET=testsecret
GOTRUE_EXTERNAL_SLACK_OIDC_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_WORKOS_ENABLED=true
GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_WORKOS_SECRET=testsecret
Expand Down
2 changes: 2 additions & 0 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
return provider.NewSpotifyProvider(config.External.Spotify, scopes)
case "slack":
return provider.NewSlackProvider(config.External.Slack, scopes)
case "slack_oidc":
return provider.NewSlackOIDCProvider(config.External.SlackOIDC, scopes)
case "twitch":
return provider.NewTwitchProvider(config.External.Twitch, scopes)
case "twitter":
Expand Down
33 changes: 33 additions & 0 deletions internal/api/external_slack_oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import (
"net/http"
"net/http/httptest"
"net/url"

jwt "github.com/golang-jwt/jwt"
)

func (ts *ExternalTestSuite) TestSignupExternalSlackOIDC() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=slack_oidc", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Slack.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Slack.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))
ts.Equal("profile email openid", q.Get("scope"))

claims := ExternalProviderClaims{}
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)

ts.Equal("slack_oidc", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
4 changes: 2 additions & 2 deletions internal/api/provider/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type slackUser struct {
TeamID string `json:"https://slack.com/team_id"`
}

// NewSlackProvider creates a Slack account provider.
// NewSlackProvider creates a Slack account provider with Legacy Slack OAuth.
func NewSlackProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
if err := ext.ValidateOAuth(); err != nil {
return nil, err
Expand Down Expand Up @@ -71,7 +71,7 @@ func (g slackProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use
if u.Email != "" {
data.Emails = []Email{{
Email: u.Email,
Verified: true, // Slack dosen't provide data on if email is verified.
Verified: true, // Slack doesn't provide data on if email is verified.
Primary: true,
}}
}
Expand Down
99 changes: 99 additions & 0 deletions internal/api/provider/slack_oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package provider

import (
"context"
"strings"

"github.com/supabase/auth/internal/conf"
"golang.org/x/oauth2"
)

const defaultSlackOIDCApiBase = "slack.com"

type slackOIDCProvider struct {
*oauth2.Config
APIPath string
}

type slackOIDCUser struct {
ID string `json:"https://slack.com/user_id"`
TeamID string `json:"https://slack.com/team_id"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
AvatarURL string `json:"picture"`
}

// NewSlackOIDCProvider creates a Slack account provider with Sign in with Slack.
func NewSlackOIDCProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
if err := ext.ValidateOAuth(); err != nil {
return nil, err
}

apiPath := chooseHost(ext.URL, defaultSlackOIDCApiBase) + "/api"
authPath := chooseHost(ext.URL, defaultSlackOIDCApiBase) + "/openid"

// these are required scopes for slack's OIDC flow
// see https://api.slack.com/authentication/sign-in-with-slack#implementation
oauthScopes := []string{
"profile",
"email",
"openid",
}

if scopes != "" {
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

return &slackOIDCProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID[0],
ClientSecret: ext.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: authPath + "/connect/authorize",
TokenURL: apiPath + "/openid.connect.token",
},
Scopes: oauthScopes,
RedirectURL: ext.RedirectURI,
},
APIPath: apiPath,
}, nil
}

func (g slackOIDCProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
return g.Exchange(context.Background(), code)
}

func (g slackOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
var u slackOIDCUser
if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/openid.connect.userInfo", &u); err != nil {
return nil, err
}

data := &UserProvidedData{}
if u.Email != "" {
data.Emails = []Email{{
Email: u.Email,
// email_verified is returned as part of the response
// see: https://api.slack.com/authentication/sign-in-with-slack#response
Verified: u.EmailVerified,
Primary: true,
}}
}

data.Metadata = &Claims{
Issuer: g.APIPath,
Subject: u.ID,
Name: u.Name,
Picture: u.AvatarURL,
CustomClaims: map[string]interface{}{
"https://slack.com/team_id": u.TeamID,
},

// To be deprecated
AvatarURL: u.AvatarURL,
FullName: u.Name,
ProviderId: u.ID,
}
return data, nil
}
2 changes: 2 additions & 0 deletions internal/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ProviderSettings struct {
Notion bool `json:"notion"`
Spotify bool `json:"spotify"`
Slack bool `json:"slack"`
SlackOIDC bool `json:"slack_oidc"`
WorkOS bool `json:"workos"`
Twitch bool `json:"twitch"`
Twitter bool `json:"twitter"`
Expand Down Expand Up @@ -62,6 +63,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Notion: config.External.Notion.Enabled,
Spotify: config.External.Spotify.Enabled,
Slack: config.External.Slack.Enabled,
SlackOIDC: config.External.SlackOIDC.Enabled,
Twitch: config.External.Twitch.Enabled,
Twitter: config.External.Twitter.Enabled,
WorkOS: config.External.WorkOS.Enabled,
Expand Down
2 changes: 2 additions & 0 deletions internal/api/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ func TestSettings_DefaultProviders(t *testing.T) {
require.True(t, p.Notion)
require.True(t, p.Spotify)
require.True(t, p.Slack)
require.True(t, p.SlackOIDC)
require.True(t, p.Google)
require.True(t, p.Kakao)
require.True(t, p.Keycloak)
require.True(t, p.Linkedin)
require.True(t, p.LinkedinOIDC)
require.True(t, p.GitHub)
require.True(t, p.GitLab)
require.True(t, p.Twitch)
Expand Down
1 change: 1 addition & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ type ProviderConfiguration struct {
LinkedinOIDC OAuthProviderConfiguration `json:"linkedin_oidc" envconfig:"LINKEDIN_OIDC"`
Spotify OAuthProviderConfiguration `json:"spotify"`
Slack OAuthProviderConfiguration `json:"slack"`
SlackOIDC OAuthProviderConfiguration `json:"slack_oidc" envconfig:"SLACK_OIDC"`
Twitter OAuthProviderConfiguration `json:"twitter"`
Twitch OAuthProviderConfiguration `json:"twitch"`
WorkOS OAuthProviderConfiguration `json:"workos"`
Expand Down

0 comments on commit bb99251

Please sign in to comment.