@@ -31,6 +31,7 @@ type ExternalProviderClaims struct {
3131 Referrer string `json:"referrer,omitempty"`
3232 FlowStateID string `json:"flow_state_id"`
3333 LinkingTargetID string `json:"linking_target_id,omitempty"`
34+ EmailOptional bool `json:"email_optional,omitempty"`
3435}
3536
3637// ExternalProviderRedirect redirects the request to the oauth provider
@@ -55,7 +56,7 @@ func (a *API) GetExternalProviderRedirectURL(w http.ResponseWriter, r *http.Requ
5556 codeChallenge := query .Get ("code_challenge" )
5657 codeChallengeMethod := query .Get ("code_challenge_method" )
5758
58- p , err := a .Provider (ctx , providerType , scopes )
59+ p , pConfig , err := a .Provider (ctx , providerType , scopes )
5960 if err != nil {
6061 return "" , apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Unsupported provider: %+v" , err ).WithInternalError (err )
6162 }
@@ -96,10 +97,11 @@ func (a *API) GetExternalProviderRedirectURL(w http.ResponseWriter, r *http.Requ
9697 SiteURL : config .SiteURL ,
9798 InstanceID : uuid .Nil .String (),
9899 },
99- Provider : providerType ,
100- InviteToken : inviteToken ,
101- Referrer : redirectURL ,
102- FlowStateID : flowStateID ,
100+ Provider : providerType ,
101+ InviteToken : inviteToken ,
102+ Referrer : redirectURL ,
103+ FlowStateID : flowStateID ,
104+ EmailOptional : pConfig .EmailOptional ,
103105 }
104106
105107 if linkingTargetUser != nil {
@@ -144,7 +146,7 @@ func (a *API) ExternalProviderCallback(w http.ResponseWriter, r *http.Request) e
144146
145147func (a * API ) handleOAuthCallback (r * http.Request ) (* OAuthProviderData , error ) {
146148 ctx := r .Context ()
147- providerType := getExternalProviderType (ctx )
149+ providerType , _ := getExternalProviderType (ctx )
148150
149151 var oAuthResponseData * OAuthProviderData
150152 var err error
@@ -168,16 +170,18 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
168170 var grantParams models.GrantParams
169171 grantParams .FillGrantParams (r )
170172
171- providerType := getExternalProviderType (ctx )
173+ providerType , emailOptional := getExternalProviderType (ctx )
172174 data , err := a .handleOAuthCallback (r )
173175 if err != nil {
174176 return err
175177 }
176178
177179 userData := data .userData
178- if len (userData .Emails ) <= 0 {
180+
181+ if len (userData .Emails ) == 0 && ! emailOptional {
179182 return apierrors .NewInternalServerError ("Error getting user email from external provider" )
180183 }
184+
181185 userData .Metadata .EmailVerified = false
182186 for _ , email := range userData .Emails {
183187 if email .Primary {
@@ -226,7 +230,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
226230 return terr
227231 }
228232 } else {
229- if user , terr = a .createAccountFromExternalIdentity (tx , r , userData , providerType ); terr != nil {
233+ if user , terr = a .createAccountFromExternalIdentity (tx , r , userData , providerType , emailOptional ); terr != nil {
230234 return terr
231235 }
232236 }
@@ -285,7 +289,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
285289 return nil
286290}
287291
288- func (a * API ) createAccountFromExternalIdentity (tx * storage.Connection , r * http.Request , userData * provider.UserProvidedData , providerType string ) (* models.User , error ) {
292+ func (a * API ) createAccountFromExternalIdentity (tx * storage.Connection , r * http.Request , userData * provider.UserProvidedData , providerType string , emailOptional bool ) (* models.User , error ) {
289293 ctx := r .Context ()
290294 aud := a .requestAud (ctx , r )
291295 config := a .config
@@ -378,8 +382,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
378382 return nil , apierrors .NewForbiddenError (apierrors .ErrorCodeUserBanned , "User is banned" )
379383 }
380384
381- // TODO(hf): Expand this boolean with all providers that may not have emails (like X/Twitter, Discord).
382- hasEmails := providerType != "web3" // intentionally not using len(userData.Emails) != 0 for better backward compatibility control
385+ hasEmails := providerType != "web3" && ! (emailOptional && decision .CandidateEmail .Email == "" )
383386
384387 if hasEmails && ! user .IsConfirmed () {
385388 // The user may have other unconfirmed email + password
@@ -400,21 +403,19 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
400403 return nil , apierrors .NewInternalServerError ("Error updating user" ).WithInternalError (terr )
401404 }
402405 } else {
403- // Some providers, like web3 don't have email data.
404- // Treat these as if a confirmation email has been
405- // sent, although the user will be created without an
406- // email address.
407406 emailConfirmationSent := false
408407 if decision .CandidateEmail .Email != "" {
409408 if terr = a .sendConfirmation (r , tx , user , models .ImplicitFlow ); terr != nil {
410409 return nil , terr
411410 }
412411 emailConfirmationSent = true
413412 }
413+
414414 if ! config .Mailer .AllowUnverifiedEmailSignIns {
415415 if emailConfirmationSent {
416416 return nil , storage .NewCommitWithError (apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeProviderEmailNeedsVerification , fmt .Sprintf ("Unverified email with %v. A confirmation email has been sent to your %v email" , providerType , providerType )))
417417 }
418+
418419 return nil , storage .NewCommitWithError (apierrors .NewUnprocessableEntityError (apierrors .ErrorCodeProviderEmailNeedsVerification , fmt .Sprintf ("Unverified email with %v. Verify the email with %v in order to sign in" , providerType , providerType )))
419420 }
420421 }
@@ -564,67 +565,97 @@ func (a *API) loadExternalState(ctx context.Context, r *http.Request) (context.C
564565 }
565566 ctx = withTargetUser (ctx , u )
566567 }
567- ctx = withExternalProviderType (ctx , claims .Provider )
568+ ctx = withExternalProviderType (ctx , claims .Provider , claims . EmailOptional )
568569 return withSignature (ctx , state ), nil
569570}
570571
571572// Provider returns a Provider interface for the given name.
572- func (a * API ) Provider (ctx context.Context , name string , scopes string ) (provider.Provider , error ) {
573+ func (a * API ) Provider (ctx context.Context , name string , scopes string ) (provider.Provider , conf. OAuthProviderConfiguration , error ) {
573574 config := a .config
574575 name = strings .ToLower (name )
575576
577+ var err error
578+ var p provider.Provider
579+ var pConfig conf.OAuthProviderConfiguration
580+
576581 switch name {
577582 case "apple" :
578- return provider .NewAppleProvider (ctx , config .External .Apple )
583+ pConfig = config .External .Apple
584+ p , err = provider .NewAppleProvider (ctx , pConfig )
579585 case "azure" :
580- return provider .NewAzureProvider (config .External .Azure , scopes )
586+ pConfig = config .External .Azure
587+ p , err = provider .NewAzureProvider (pConfig , scopes )
581588 case "bitbucket" :
582- return provider .NewBitbucketProvider (config .External .Bitbucket )
589+ pConfig = config .External .Bitbucket
590+ p , err = provider .NewBitbucketProvider (pConfig )
583591 case "discord" :
584- return provider .NewDiscordProvider (config .External .Discord , scopes )
592+ pConfig = config .External .Discord
593+ p , err = provider .NewDiscordProvider (pConfig , scopes )
585594 case "facebook" :
586- return provider .NewFacebookProvider (config .External .Facebook , scopes )
595+ pConfig = config .External .Facebook
596+ p , err = provider .NewFacebookProvider (pConfig , scopes )
587597 case "figma" :
588- return provider .NewFigmaProvider (config .External .Figma , scopes )
598+ pConfig = config .External .Figma
599+ p , err = provider .NewFigmaProvider (pConfig , scopes )
589600 case "fly" :
590- return provider .NewFlyProvider (config .External .Fly , scopes )
601+ pConfig = config .External .Fly
602+ p , err = provider .NewFlyProvider (pConfig , scopes )
591603 case "github" :
592- return provider .NewGithubProvider (config .External .Github , scopes )
604+ pConfig = config .External .Github
605+ p , err = provider .NewGithubProvider (pConfig , scopes )
593606 case "gitlab" :
594- return provider .NewGitlabProvider (config .External .Gitlab , scopes )
607+ pConfig = config .External .Gitlab
608+ p , err = provider .NewGitlabProvider (pConfig , scopes )
595609 case "google" :
596- return provider .NewGoogleProvider (ctx , config .External .Google , scopes )
610+ pConfig = config .External .Google
611+ p , err = provider .NewGoogleProvider (ctx , pConfig , scopes )
597612 case "kakao" :
598- return provider .NewKakaoProvider (config .External .Kakao , scopes )
613+ pConfig = config .External .Kakao
614+ p , err = provider .NewKakaoProvider (pConfig , scopes )
599615 case "keycloak" :
600- return provider .NewKeycloakProvider (config .External .Keycloak , scopes )
616+ pConfig = config .External .Keycloak
617+ p , err = provider .NewKeycloakProvider (pConfig , scopes )
601618 case "linkedin" :
602- return provider .NewLinkedinProvider (config .External .Linkedin , scopes )
619+ pConfig = config .External .Linkedin
620+ p , err = provider .NewLinkedinProvider (pConfig , scopes )
603621 case "linkedin_oidc" :
604- return provider .NewLinkedinOIDCProvider (config .External .LinkedinOIDC , scopes )
622+ pConfig = config .External .LinkedinOIDC
623+ p , err = provider .NewLinkedinOIDCProvider (pConfig , scopes )
605624 case "notion" :
606- return provider .NewNotionProvider (config .External .Notion )
625+ pConfig = config .External .Notion
626+ p , err = provider .NewNotionProvider (pConfig )
607627 case "snapchat" :
608- return provider .NewSnapchatProvider (config .External .Snapchat , scopes )
628+ pConfig = config .External .Snapchat
629+ p , err = provider .NewSnapchatProvider (pConfig , scopes )
609630 case "spotify" :
610- return provider .NewSpotifyProvider (config .External .Spotify , scopes )
631+ pConfig = config .External .Spotify
632+ p , err = provider .NewSpotifyProvider (pConfig , scopes )
611633 case "slack" :
612- return provider .NewSlackProvider (config .External .Slack , scopes )
634+ pConfig = config .External .Slack
635+ p , err = provider .NewSlackProvider (pConfig , scopes )
613636 case "slack_oidc" :
614- return provider .NewSlackOIDCProvider (config .External .SlackOIDC , scopes )
637+ pConfig = config .External .SlackOIDC
638+ p , err = provider .NewSlackOIDCProvider (pConfig , scopes )
615639 case "twitch" :
616- return provider .NewTwitchProvider (config .External .Twitch , scopes )
640+ pConfig = config .External .Twitch
641+ p , err = provider .NewTwitchProvider (pConfig , scopes )
617642 case "twitter" :
618- return provider .NewTwitterProvider (config .External .Twitter , scopes )
643+ pConfig = config .External .Twitter
644+ p , err = provider .NewTwitterProvider (pConfig , scopes )
619645 case "vercel_marketplace" :
620- return provider .NewVercelMarketplaceProvider (config .External .VercelMarketplace , scopes )
646+ pConfig = config .External .VercelMarketplace
647+ p , err = provider .NewVercelMarketplaceProvider (pConfig , scopes )
621648 case "workos" :
622- return provider .NewWorkOSProvider (config .External .WorkOS )
649+ pConfig = config .External .WorkOS
650+ p , err = provider .NewWorkOSProvider (pConfig )
623651 case "zoom" :
624- return provider .NewZoomProvider (config .External .Zoom )
652+ pConfig = config .External .Zoom
653+ p , err = provider .NewZoomProvider (pConfig )
625654 default :
626- return nil , fmt .Errorf ("Provider %s could not be found" , name )
655+ return nil , pConfig , fmt .Errorf ("Provider %s could not be found" , name )
627656 }
657+
658+ return p , pConfig , err
628659}
629660
630661func redirectErrors (handler apiHandler , w http.ResponseWriter , r * http.Request , u * url.URL ) {
0 commit comments