[MM-67791] Use atomic token consumption for guest magic links (#35489)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions

#### Summary

Use the atomic `ConsumeOnce` pattern for guest magic link token consumption, consistent with how SSO code exchange tokens are already handled.

#### Ticket Link

https://mattermost.atlassian.net/browse/MM-67791

#### Release Note

```release-note
Improved token handling in the guest magic link authentication flow.
```
This commit is contained in:
edgarbellot 2026-03-06 10:47:55 +01:00 committed by GitHub
parent 56f51d7df2
commit f542d7ca18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 14 additions and 20 deletions

View file

@ -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() {

View file

@ -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) {

View file

@ -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."