enhance oauth for edge cases
This commit is contained in:
@@ -178,8 +178,12 @@ var ErrOAuthUnavailable = errors.New("oauth service unavailable")
|
|||||||
//
|
//
|
||||||
// 1. Existing linkage on (provider, provider_user_id) → log that user in.
|
// 1. Existing linkage on (provider, provider_user_id) → log that user in.
|
||||||
// 2. Existing user with same email → link this provider to them, log in.
|
// 2. Existing user with same email → link this provider to them, log in.
|
||||||
// 3. Otherwise create a new user (no password, email_verified iff provider
|
// 3. Otherwise create a new user (no password, email_verified) and link.
|
||||||
// said so) and link.
|
//
|
||||||
|
// Every successful OAuth round-trip is treated as proof the user controls
|
||||||
|
// the email address — Google + Facebook both verify email ownership before
|
||||||
|
// returning it to us, so we always mark the email as verified on sign-in
|
||||||
|
// regardless of the provider's `email_verified` flag.
|
||||||
//
|
//
|
||||||
// In every case the resulting user gets a fresh JWT just like LogIn.
|
// In every case the resulting user gets a fresh JWT just like LogIn.
|
||||||
func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*AuthResult, error) {
|
func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*AuthResult, error) {
|
||||||
@@ -197,6 +201,7 @@ func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*
|
|||||||
// avatar may have changed at the provider).
|
// avatar may have changed at the provider).
|
||||||
_, _ = s.oauth.Link(ctx, u.ID, prof.Provider, prof.ProviderUserID,
|
_, _ = s.oauth.Link(ctx, u.ID, prof.Provider, prof.ProviderUserID,
|
||||||
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL))
|
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL))
|
||||||
|
s.ensureEmailVerified(ctx, u)
|
||||||
return s.issueSession(u)
|
return s.issueSession(u)
|
||||||
} else if !errors.Is(err, repository.ErrOAuthAccountNotFound) {
|
} else if !errors.Is(err, repository.ErrOAuthAccountNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -214,6 +219,7 @@ func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*
|
|||||||
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL)); err != nil {
|
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
s.ensureEmailVerified(ctx, u)
|
||||||
return s.issueSession(u)
|
return s.issueSession(u)
|
||||||
} else if !errors.Is(err, repository.ErrUserNotFound) {
|
} else if !errors.Is(err, repository.ErrUserNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -232,7 +238,8 @@ func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*
|
|||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
handle = fmt.Sprintf("%s-%s", base, randomHandleSuffix())
|
handle = fmt.Sprintf("%s-%s", base, randomHandleSuffix())
|
||||||
}
|
}
|
||||||
u, err = s.users.CreateOAuth(ctx, prof.Email, displayName, handle, optStr(prof.AvatarURL), prof.EmailVerified)
|
// Always create as verified — successful OAuth = email ownership proof.
|
||||||
|
u, err = s.users.CreateOAuth(ctx, prof.Email, displayName, handle, optStr(prof.AvatarURL), true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -241,6 +248,7 @@ func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (*
|
|||||||
if u2, err2 := s.users.FindByEmail(ctx, prof.Email); err2 == nil {
|
if u2, err2 := s.users.FindByEmail(ctx, prof.Email); err2 == nil {
|
||||||
_, _ = s.oauth.Link(ctx, u2.ID, prof.Provider, prof.ProviderUserID,
|
_, _ = s.oauth.Link(ctx, u2.ID, prof.Provider, prof.ProviderUserID,
|
||||||
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL))
|
optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL))
|
||||||
|
s.ensureEmailVerified(ctx, u2)
|
||||||
return s.issueSession(u2)
|
return s.issueSession(u2)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -269,6 +277,23 @@ func optStr(s string) *string {
|
|||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureEmailVerified flips email_verified_at to now() on the given user
|
||||||
|
// if it's not already set. Called from every SignInWithOAuth branch so a
|
||||||
|
// successful OAuth round-trip immediately satisfies email verification
|
||||||
|
// for an existing-but-unverified account. Mutates u in place so callers
|
||||||
|
// see the updated timestamp without re-fetching.
|
||||||
|
func (s *AuthService) ensureEmailVerified(ctx context.Context, u *model.User) {
|
||||||
|
if u == nil || u.EmailVerifiedAt != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.users.MarkEmailVerified(ctx, u.ID); err != nil {
|
||||||
|
s.logger.Warn("mark email verified after oauth", zap.String("user", u.ID), zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
u.EmailVerifiedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
// Me looks up a user by id and re-verifies the JWT's token_version against
|
// Me looks up a user by id and re-verifies the JWT's token_version against
|
||||||
// the stored value. Mismatch means the user has logged-out-everywhere or
|
// the stored value. Mismatch means the user has logged-out-everywhere or
|
||||||
// changed password since this JWT was issued → reject.
|
// changed password since this JWT was issued → reject.
|
||||||
|
|||||||
Reference in New Issue
Block a user