diff --git a/server/channels/app/user.go b/server/channels/app/user.go index 521e62ff719..e142d9bebcb 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -125,20 +125,15 @@ func (a *App) CreateUserWithToken(rctx request.CTX, user *model.User, token *mod // This function handles the passwordless "guest magic link" flow where clicking an email link logs the user in. // Follows the same pattern as SAML/OAuth SSO by creating the user then calling AddUserToTeamByToken. func (a *App) AuthenticateUserForGuestMagicLink(rctx request.CTX, tokenString string) (*model.User, *model.AppError) { - // Get and validate token type and expiry - token, err := a.Srv().Store().Token().GetByToken(tokenString) + // Atomically consume the token to prevent race conditions where concurrent + // requests could reuse the same single-use token to create multiple sessions. + // Try both valid token types for guest magic links. + token, err := a.ConsumeTokenOnce(model.TokenTypeGuestMagicLinkInvitation, tokenString) if err != nil { - return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token.app_error", nil, "", http.StatusBadRequest).Wrap(err) - } - - if token.Type != model.TokenTypeGuestMagicLinkInvitation && token.Type != model.TokenTypeGuestMagicLink { - return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token_type.app_error", nil, "", http.StatusBadRequest) - } - - // We have the token we were looking for, so remove it from the database ASAP - err = a.Srv().Store().Token().Delete(tokenString) - if err != nil { - rctx.Logger().Warn("Error while deleting token", mlog.Err(err)) + token, err = a.ConsumeTokenOnce(model.TokenTypeGuestMagicLink, tokenString) + if err != nil { + return nil, model.NewAppError("AuthenticateUserForGuestMagicLink", "api.user.guest_magic_link.invalid_token.app_error", nil, "", http.StatusBadRequest).Wrap(err) + } } if token.IsExpired() { diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index 07656ced9fe..74553a7f225 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -2687,14 +2687,17 @@ func TestAuthenticateUserForGuestMagicLink(t *testing.T) { ) require.NoError(t, th.App.Srv().Store().Token().Save(token)) defer func() { - appErr := th.App.DeleteToken(token) - require.Nil(t, appErr) + _ = th.App.Srv().Store().Token().Delete(token.Token) }() user, err := th.App.AuthenticateUserForGuestMagicLink(th.Context, token.Token) require.NotNil(t, err, "Should fail on wrong token type") require.Nil(t, user) - assert.Equal(t, "api.user.guest_magic_link.invalid_token_type.app_error", err.Id) + assert.Equal(t, "api.user.guest_magic_link.invalid_token.app_error", err.Id) + + // Verify token was NOT consumed (wrong type should not delete it) + _, nErr := th.App.Srv().Store().Token().GetByToken(token.Token) + require.NoError(t, nErr, "Token with wrong type should still exist") }) t.Run("user already exists returns generic error", func(t *testing.T) { diff --git a/server/i18n/en.json b/server/i18n/en.json index ec29cf95ec1..88d4b3ef3cc 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -4446,10 +4446,6 @@ "id": "api.user.guest_magic_link.invalid_token.app_error", "translation": "Invalid invitation link." }, - { - "id": "api.user.guest_magic_link.invalid_token_type.app_error", - "translation": "Invalid invitation link type." - }, { "id": "api.user.guest_magic_link.missing_token.app_error", "translation": "Missing invitation token."