Gate password sign-in on email verification

Unverified password identities were able to open sessions after
signing out. Reject sign-in with EMAIL_NOT_VERIFIED and add a
resend-confirmation flow so users can complete verification.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-28 10:15:25 +02:00
parent bb9fb22913
commit 5d0882778f
19 changed files with 621 additions and 60 deletions

View File

@@ -120,7 +120,7 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
confirmationToken, err := statelesstoken.NewToken(
s.tokenSecret,
TokenTypeEmailConfirmation,
24*time.Hour,
s.emailConfirmationTokenValidity,
EmailConfirmationData{IdentityID: identityID, Email: req.NewEmail},
)
if err != nil {
@@ -226,6 +226,58 @@ func (s AccountService) VerifyEmail(ctx context.Context, token string) error {
)
}
func (s AccountService) ResendVerificationEmail(ctx context.Context, email mail.Addr) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
identity := &coredata.Identity{}
if err := identity.LoadByEmail(ctx, tx, email); err != nil {
if err == coredata.ErrResourceNotFound {
return nil // Don't leak information about non-existent identities
}
return fmt.Errorf("cannot load identity: %w", err)
}
if identity.EmailAddressVerified {
return nil // Don't leak information about already-verified identities
}
confirmationToken, err := statelesstoken.NewToken(
s.tokenSecret,
TokenTypeEmailConfirmation,
s.emailConfirmationTokenValidity,
EmailConfirmationData{IdentityID: identity.ID, Email: identity.EmailAddress},
)
if err != nil {
return fmt.Errorf("cannot generate confirmation token: %w", err)
}
emailPresenter := emails.NewPresenter(s.baseURL, identity.FullName)
subject, textBody, htmlBody, err := emailPresenter.RenderConfirmEmail(ctx, "/auth/verify-email", confirmationToken)
if err != nil {
return fmt.Errorf("cannot render confirmation email: %w", err)
}
confirmationEmail := coredata.NewEmail(
identity.FullName,
identity.EmailAddress,
subject,
textBody,
htmlBody,
nil,
)
if err := confirmationEmail.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert confirmation email: %w", err)
}
return nil
},
)
}
func (s *AccountService) ListPendingInvitations(
ctx context.Context,
userID gid.GID,