From 59b3687e43503f7316c87cf96bb4f64fdb04cd6b Mon Sep 17 00:00:00 2001 From: Krispire Admin Date: Tue, 2 Jun 2026 15:21:32 +0530 Subject: [PATCH] enhance oauth for edge cases --- internal/service/auth_service.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index d0045ba..89cafc5 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -178,8 +178,12 @@ var ErrOAuthUnavailable = errors.New("oauth service unavailable") // // 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. -// 3. Otherwise create a new user (no password, email_verified iff provider -// said so) and link. +// 3. Otherwise create a new user (no password, email_verified) 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. 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). _, _ = s.oauth.Link(ctx, u.ID, prof.Provider, prof.ProviderUserID, optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL)) + s.ensureEmailVerified(ctx, u) return s.issueSession(u) } else if !errors.Is(err, repository.ErrOAuthAccountNotFound) { 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 { return nil, err } + s.ensureEmailVerified(ctx, u) return s.issueSession(u) } else if !errors.Is(err, repository.ErrUserNotFound) { return nil, err @@ -232,7 +238,8 @@ func (s *AuthService) SignInWithOAuth(ctx context.Context, prof OAuthProfile) (* if attempt > 0 { 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 { 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 { _, _ = s.oauth.Link(ctx, u2.ID, prof.Provider, prof.ProviderUserID, optStr(prof.Email), optStr(prof.Name), optStr(prof.AvatarURL)) + s.ensureEmailVerified(ctx, u2) return s.issueSession(u2) } return nil, err @@ -269,6 +277,23 @@ func optStr(s string) *string { 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 // the stored value. Mismatch means the user has logged-out-everywhere or // changed password since this JWT was issued → reject.