Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update to Slack OAuth V2 #1591

Merged
merged 11 commits into from
Jun 12, 2024
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
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", 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
94 changes: 94 additions & 0 deletions internal/api/provider/slack_oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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"`
Email string `json:"email"`
Name string `json:"name"`
AvatarURL string `json:"picture"`
TeamID string `json:"https://slack.com/team_id"`
}

// 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"

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,
Verified: true, // Slack doesn't provide data on if email is verified.
zhawtof marked this conversation as resolved.
Show resolved Hide resolved
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
}
1 change: 1 addition & 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
1 change: 1 addition & 0 deletions internal/api/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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)
Expand Down
1 change: 1 addition & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,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
Loading