Distinguish expired magic links from invalid tokens

When a magic link token expires, the user now sees a specific
error message ("This magic link has expired. Please request a
new one.") instead of the generic "Failed to connect" error.

This adds ErrExpiredToken to the IAM error types, checks for
statelesstoken.ErrExpiredToken in both GetMagicLinkEmail and
OpenSessionWithMagicLink, and handles it in the trust resolver
and frontend.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-26 23:56:17 +01:00
parent 36d517e8ce
commit 8f59eb6b8a
4 changed files with 43 additions and 1 deletions

View File

@@ -634,6 +634,11 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
func (s AuthService) GetMagicLinkEmail(ctx context.Context, tokenString string) (mail.Addr, error) {
payload, err := statelesstoken.ValidateToken[MagicLinkData](s.tokenSecret, TokenTypeMagicLink, tokenString)
if err != nil {
var errExpired *statelesstoken.ErrExpiredToken
if errors.As(err, &errExpired) {
return mail.Nil, NewExpiredTokenError()
}
return mail.Nil, NewInvalidTokenError()
}
@@ -649,6 +654,11 @@ func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, tokenString s
payload, err := statelesstoken.ValidateToken[MagicLinkData](s.tokenSecret, TokenTypeMagicLink, tokenString)
if err != nil {
var errExpired *statelesstoken.ErrExpiredToken
if errors.As(err, &errExpired) {
return nil, nil, nil, NewExpiredTokenError()
}
return nil, nil, nil, NewInvalidTokenError()
}

View File

@@ -31,6 +31,16 @@ func (e ErrInvalidToken) Error() string {
return e.message
}
type ErrExpiredToken struct{ message string }
func NewExpiredTokenError() error {
return &ErrExpiredToken{"token has expired"}
}
func (e ErrExpiredToken) Error() string {
return e.message
}
type ErrInvitationAlreadyAccepted struct{ InvitationID gid.GID }
func NewInvitationAlreadyAcceptedError(invitationID gid.GID) error {

View File

@@ -225,6 +225,11 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri
email, err := r.iam.AuthService.GetMagicLinkEmail(ctx, input.Token)
if err != nil {
var errExpiredToken *iam.ErrExpiredToken
if errors.As(err, &errExpiredToken) {
return nil, gqlutils.Invalid(ctx, err)
}
var errInvalidToken *iam.ErrInvalidToken
if errors.As(err, &errInvalidToken) {
return nil, gqlutils.Invalid(ctx, err)
@@ -241,6 +246,11 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri
var err error
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
if err != nil {
var errExpiredToken *iam.ErrExpiredToken
if errors.As(err, &errExpiredToken) {
return nil, gqlutils.Invalid(ctx, err)
}
var errInvalidToken *iam.ErrInvalidToken
if errors.As(err, &errInvalidToken) {
return nil, gqlutils.Invalid(ctx, err)
@@ -258,6 +268,11 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri
var err error
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
if err != nil {
var errExpiredToken *iam.ErrExpiredToken
if errors.As(err, &errExpiredToken) {
return nil, gqlutils.Invalid(ctx, err)
}
var errInvalidToken *iam.ErrInvalidToken
if errors.As(err, &errInvalidToken) {
return nil, gqlutils.Invalid(ctx, err)