@@ -31,6 +31,7 @@ type ExternalProviderClaims struct {
31
31
Referrer string `json:"referrer,omitempty"`
32
32
FlowStateID string `json:"flow_state_id"`
33
33
LinkingTargetID string `json:"linking_target_id,omitempty"`
34
+ EmailOptional bool `json:"email_optional,omitempty"`
34
35
}
35
36
36
37
// ExternalProviderRedirect redirects the request to the oauth provider
@@ -55,7 +56,7 @@ func (a *API) GetExternalProviderRedirectURL(w http.ResponseWriter, r *http.Requ
55
56
codeChallenge := query .Get ("code_challenge" )
56
57
codeChallengeMethod := query .Get ("code_challenge_method" )
57
58
58
- p , err := a .Provider (ctx , providerType , scopes )
59
+ p , pConfig , err := a .Provider (ctx , providerType , scopes )
59
60
if err != nil {
60
61
return "" , apierrors .NewBadRequestError (apierrors .ErrorCodeValidationFailed , "Unsupported provider: %+v" , err ).WithInternalError (err )
61
62
}
@@ -96,10 +97,11 @@ func (a *API) GetExternalProviderRedirectURL(w http.ResponseWriter, r *http.Requ
96
97
SiteURL : config .SiteURL ,
97
98
InstanceID : uuid .Nil .String (),
98
99
},
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 ,
103
105
}
104
106
105
107
if linkingTargetUser != nil {
@@ -144,7 +146,7 @@ func (a *API) ExternalProviderCallback(w http.ResponseWriter, r *http.Request) e
144
146
145
147
func (a * API ) handleOAuthCallback (r * http.Request ) (* OAuthProviderData , error ) {
146
148
ctx := r .Context ()
147
- providerType := getExternalProviderType (ctx )
149
+ providerType , _ := getExternalProviderType (ctx )
148
150
149
151
var oAuthResponseData * OAuthProviderData
150
152
var err error
@@ -168,16 +170,18 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
168
170
var grantParams models.GrantParams
169
171
grantParams .FillGrantParams (r )
170
172
171
- providerType := getExternalProviderType (ctx )
173
+ providerType , emailOptional := getExternalProviderType (ctx )
172
174
data , err := a .handleOAuthCallback (r )
173
175
if err != nil {
174
176
return err
175
177
}
176
178
177
179
userData := data .userData
178
- if len (userData .Emails ) <= 0 {
180
+
181
+ if len (userData .Emails ) == 0 && ! emailOptional {
179
182
return apierrors .NewInternalServerError ("Error getting user email from external provider" )
180
183
}
184
+
181
185
userData .Metadata .EmailVerified = false
182
186
for _ , email := range userData .Emails {
183
187
if email .Primary {
@@ -226,7 +230,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
226
230
return terr
227
231
}
228
232
} 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 {
230
234
return terr
231
235
}
232
236
}
@@ -285,7 +289,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
285
289
return nil
286
290
}
287
291
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 ) {
289
293
ctx := r .Context ()
290
294
aud := a .requestAud (ctx , r )
291
295
config := a .config
@@ -378,8 +382,7 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
378
382
return nil , apierrors .NewForbiddenError (apierrors .ErrorCodeUserBanned , "User is banned" )
379
383
}
380
384
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 == "" )
383
386
384
387
if hasEmails && ! user .IsConfirmed () {
385
388
// The user may have other unconfirmed email + password
@@ -400,21 +403,19 @@ func (a *API) createAccountFromExternalIdentity(tx *storage.Connection, r *http.
400
403
return nil , apierrors .NewInternalServerError ("Error updating user" ).WithInternalError (terr )
401
404
}
402
405
} 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.
407
406
emailConfirmationSent := false
408
407
if decision .CandidateEmail .Email != "" {
409
408
if terr = a .sendConfirmation (r , tx , user , models .ImplicitFlow ); terr != nil {
410
409
return nil , terr
411
410
}
412
411
emailConfirmationSent = true
413
412
}
413
+
414
414
if ! config .Mailer .AllowUnverifiedEmailSignIns {
415
415
if emailConfirmationSent {
416
416
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 )))
417
417
}
418
+
418
419
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 )))
419
420
}
420
421
}
@@ -564,67 +565,97 @@ func (a *API) loadExternalState(ctx context.Context, r *http.Request) (context.C
564
565
}
565
566
ctx = withTargetUser (ctx , u )
566
567
}
567
- ctx = withExternalProviderType (ctx , claims .Provider )
568
+ ctx = withExternalProviderType (ctx , claims .Provider , claims . EmailOptional )
568
569
return withSignature (ctx , state ), nil
569
570
}
570
571
571
572
// 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 ) {
573
574
config := a .config
574
575
name = strings .ToLower (name )
575
576
577
+ var err error
578
+ var p provider.Provider
579
+ var pConfig conf.OAuthProviderConfiguration
580
+
576
581
switch name {
577
582
case "apple" :
578
- return provider .NewAppleProvider (ctx , config .External .Apple )
583
+ pConfig = config .External .Apple
584
+ p , err = provider .NewAppleProvider (ctx , pConfig )
579
585
case "azure" :
580
- return provider .NewAzureProvider (config .External .Azure , scopes )
586
+ pConfig = config .External .Azure
587
+ p , err = provider .NewAzureProvider (pConfig , scopes )
581
588
case "bitbucket" :
582
- return provider .NewBitbucketProvider (config .External .Bitbucket )
589
+ pConfig = config .External .Bitbucket
590
+ p , err = provider .NewBitbucketProvider (pConfig )
583
591
case "discord" :
584
- return provider .NewDiscordProvider (config .External .Discord , scopes )
592
+ pConfig = config .External .Discord
593
+ p , err = provider .NewDiscordProvider (pConfig , scopes )
585
594
case "facebook" :
586
- return provider .NewFacebookProvider (config .External .Facebook , scopes )
595
+ pConfig = config .External .Facebook
596
+ p , err = provider .NewFacebookProvider (pConfig , scopes )
587
597
case "figma" :
588
- return provider .NewFigmaProvider (config .External .Figma , scopes )
598
+ pConfig = config .External .Figma
599
+ p , err = provider .NewFigmaProvider (pConfig , scopes )
589
600
case "fly" :
590
- return provider .NewFlyProvider (config .External .Fly , scopes )
601
+ pConfig = config .External .Fly
602
+ p , err = provider .NewFlyProvider (pConfig , scopes )
591
603
case "github" :
592
- return provider .NewGithubProvider (config .External .Github , scopes )
604
+ pConfig = config .External .Github
605
+ p , err = provider .NewGithubProvider (pConfig , scopes )
593
606
case "gitlab" :
594
- return provider .NewGitlabProvider (config .External .Gitlab , scopes )
607
+ pConfig = config .External .Gitlab
608
+ p , err = provider .NewGitlabProvider (pConfig , scopes )
595
609
case "google" :
596
- return provider .NewGoogleProvider (ctx , config .External .Google , scopes )
610
+ pConfig = config .External .Google
611
+ p , err = provider .NewGoogleProvider (ctx , pConfig , scopes )
597
612
case "kakao" :
598
- return provider .NewKakaoProvider (config .External .Kakao , scopes )
613
+ pConfig = config .External .Kakao
614
+ p , err = provider .NewKakaoProvider (pConfig , scopes )
599
615
case "keycloak" :
600
- return provider .NewKeycloakProvider (config .External .Keycloak , scopes )
616
+ pConfig = config .External .Keycloak
617
+ p , err = provider .NewKeycloakProvider (pConfig , scopes )
601
618
case "linkedin" :
602
- return provider .NewLinkedinProvider (config .External .Linkedin , scopes )
619
+ pConfig = config .External .Linkedin
620
+ p , err = provider .NewLinkedinProvider (pConfig , scopes )
603
621
case "linkedin_oidc" :
604
- return provider .NewLinkedinOIDCProvider (config .External .LinkedinOIDC , scopes )
622
+ pConfig = config .External .LinkedinOIDC
623
+ p , err = provider .NewLinkedinOIDCProvider (pConfig , scopes )
605
624
case "notion" :
606
- return provider .NewNotionProvider (config .External .Notion )
625
+ pConfig = config .External .Notion
626
+ p , err = provider .NewNotionProvider (pConfig )
607
627
case "snapchat" :
608
- return provider .NewSnapchatProvider (config .External .Snapchat , scopes )
628
+ pConfig = config .External .Snapchat
629
+ p , err = provider .NewSnapchatProvider (pConfig , scopes )
609
630
case "spotify" :
610
- return provider .NewSpotifyProvider (config .External .Spotify , scopes )
631
+ pConfig = config .External .Spotify
632
+ p , err = provider .NewSpotifyProvider (pConfig , scopes )
611
633
case "slack" :
612
- return provider .NewSlackProvider (config .External .Slack , scopes )
634
+ pConfig = config .External .Slack
635
+ p , err = provider .NewSlackProvider (pConfig , scopes )
613
636
case "slack_oidc" :
614
- return provider .NewSlackOIDCProvider (config .External .SlackOIDC , scopes )
637
+ pConfig = config .External .SlackOIDC
638
+ p , err = provider .NewSlackOIDCProvider (pConfig , scopes )
615
639
case "twitch" :
616
- return provider .NewTwitchProvider (config .External .Twitch , scopes )
640
+ pConfig = config .External .Twitch
641
+ p , err = provider .NewTwitchProvider (pConfig , scopes )
617
642
case "twitter" :
618
- return provider .NewTwitterProvider (config .External .Twitter , scopes )
643
+ pConfig = config .External .Twitter
644
+ p , err = provider .NewTwitterProvider (pConfig , scopes )
619
645
case "vercel_marketplace" :
620
- return provider .NewVercelMarketplaceProvider (config .External .VercelMarketplace , scopes )
646
+ pConfig = config .External .VercelMarketplace
647
+ p , err = provider .NewVercelMarketplaceProvider (pConfig , scopes )
621
648
case "workos" :
622
- return provider .NewWorkOSProvider (config .External .WorkOS )
649
+ pConfig = config .External .WorkOS
650
+ p , err = provider .NewWorkOSProvider (pConfig )
623
651
case "zoom" :
624
- return provider .NewZoomProvider (config .External .Zoom )
652
+ pConfig = config .External .Zoom
653
+ p , err = provider .NewZoomProvider (pConfig )
625
654
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 )
627
656
}
657
+
658
+ return p , pConfig , err
628
659
}
629
660
630
661
func redirectErrors (handler apiHandler , w http.ResponseWriter , r * http.Request , u * url.URL ) {
0 commit comments