diff --git a/internal/api/external.go b/internal/api/external.go index 4df4c6502..f941f55e5 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -574,6 +574,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewTwitchProvider(config.External.Twitch, scopes) case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) + case "vercel_marketplace": + return provider.NewVercelMarketplaceProvider(config.External.VercelMarketplace, scopes) case "workos": return provider.NewWorkOSProvider(config.External.WorkOS) case "zoom": diff --git a/internal/api/provider/oidc.go b/internal/api/provider/oidc.go index 51deaeff3..51c88e639 100644 --- a/internal/api/provider/oidc.go +++ b/internal/api/provider/oidc.go @@ -61,6 +61,8 @@ func ParseIDToken(ctx context.Context, provider *oidc.Provider, config *oidc.Con token, data, err = parseLinkedinIDToken(token) case IssuerKakao: token, data, err = parseKakaoIDToken(token) + case IssuerVercelMarketplace: + token, data, err = parseVercelMarketplaceIDToken(token) default: if IsAzureIssuer(token.Issuer) { token, data, err = parseAzureIDToken(token) @@ -351,6 +353,40 @@ func parseKakaoIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, e return token, &data, nil } +type VercelMarketplaceIDTokenClaims struct { + jwt.RegisteredClaims + + UserEmail string `json:"user_email"` + UserName string `json:"user_name"` + UserAvatarUrl string `json:"user_avatar_url"` +} + +func parseVercelMarketplaceIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) { + var claims VercelMarketplaceIDTokenClaims + + if err := token.Claims(&claims); err != nil { + return nil, nil, err + } + + var data UserProvidedData + + data.Emails = append(data.Emails, Email{ + Email: claims.UserEmail, + Verified: true, + Primary: true, + }) + + data.Metadata = &Claims{ + Issuer: token.Issuer, + Subject: token.Subject, + ProviderId: token.Subject, + Name: claims.UserName, + Picture: claims.UserAvatarUrl, + } + + return token, &data, nil +} + func parseGenericIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) { var data UserProvidedData diff --git a/internal/api/provider/vercel_marketplace.go b/internal/api/provider/vercel_marketplace.go new file mode 100644 index 000000000..ba76a7412 --- /dev/null +++ b/internal/api/provider/vercel_marketplace.go @@ -0,0 +1,78 @@ +package provider + +import ( + "context" + "errors" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/supabase/auth/internal/conf" + "golang.org/x/oauth2" +) + +const ( + defaultVercelMarketplaceAPIBase = "api.vercel.com" + IssuerVercelMarketplace = "https://marketplace.vercel.com" +) + +type vercelMarketplaceProvider struct { + *oauth2.Config + oidc *oidc.Provider + APIPath string +} + +// NewVercelMarketplaceProvider creates a VercelMarketplace account provider via OIDC. +func NewVercelMarketplaceProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.ValidateOAuth(); err != nil { + return nil, err + } + + apiPath := chooseHost(ext.URL, defaultVercelMarketplaceAPIBase) + + oauthScopes := []string{} + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + oidcProvider, err := oidc.NewProvider(context.Background(), IssuerVercelMarketplace) + if err != nil { + return nil, err + } + + return &vercelMarketplaceProvider{ + oidc: oidcProvider, + Config: &oauth2.Config{ + ClientID: ext.ClientID[0], + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: apiPath + "/oauth/v2/authorization", + TokenURL: apiPath + "/oauth/v2/accessToken", + }, + Scopes: oauthScopes, + RedirectURL: ext.RedirectURI, + }, + APIPath: apiPath, + }, nil +} + +func (g vercelMarketplaceProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + return g.Exchange(context.Background(), code) +} + +func (g vercelMarketplaceProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + idToken := tok.Extra("id_token") + if tok.AccessToken == "" || idToken == nil { + return nil, errors.New("vercel_marketplace: no OIDC ID token present in response") + } + + _, data, err := ParseIDToken(ctx, g.oidc, &oidc.Config{ + ClientID: g.ClientID, + }, idToken.(string), ParseIDTokenOptions{ + AccessToken: tok.AccessToken, + }) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/internal/api/token_oidc.go b/internal/api/token_oidc.go index 7b0a8155b..f94a67788 100644 --- a/internal/api/token_oidc.go +++ b/internal/api/token_oidc.go @@ -80,6 +80,12 @@ func (p *IdTokenGrantParams) getProvider(ctx context.Context, config *conf.Globa issuer = provider.IssuerKakao acceptableClientIDs = append(acceptableClientIDs, config.External.Kakao.ClientID...) + case p.Provider == "vercel_marketplace" || p.Issuer == provider.IssuerVercelMarketplace: + cfg = &config.External.VercelMarketplace + providerType = "vercel_marketplace" + issuer = provider.IssuerVercelMarketplace + acceptableClientIDs = append(acceptableClientIDs, config.External.VercelMarketplace.ClientID...) + default: log.WithField("issuer", p.Issuer).WithField("client_id", p.ClientID).Warn("Use of POST /token with arbitrary issuer and client_id is deprecated for security reasons. Please switch to using the API with provider only!") diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 21216fedb..8426616c2 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -322,6 +322,7 @@ type ProviderConfiguration struct { SlackOIDC OAuthProviderConfiguration `json:"slack_oidc" envconfig:"SLACK_OIDC"` Twitter OAuthProviderConfiguration `json:"twitter"` Twitch OAuthProviderConfiguration `json:"twitch"` + VercelMarketplace OAuthProviderConfiguration `json:"vercel_marketplace" split_words:"true"` WorkOS OAuthProviderConfiguration `json:"workos"` Email EmailProviderConfiguration `json:"email"` Phone PhoneProviderConfiguration `json:"phone"`