{"record":{"id":"2ad0f47815808b1b","repo":"bitwarden/server","slug":"useridandtokenmismatch","errorCode":null,"errorMessage":"UserIdAndTokenMismatch","messagePattern":"UserIdAndTokenMismatch","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"bitwarden_license/src/Sso/Controllers/AccountController.cs","lineNumber":808,"sourceCode":"        }\n\n        var userId = split[0];\n        var token = split[1];\n\n        var tokenOptions = new TokenOptions();\n\n        var claimedUser = await _userService.GetUserByIdAsync(userId);\n        if (claimedUser != null)\n        {\n            var tokenIsValid = await _userManager.VerifyUserTokenAsync(\n                claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);\n            if (tokenIsValid)\n            {\n                user = claimedUser;\n            }\n            else\n            {\n                throw new Exception(_i18nService.T(\"UserIdAndTokenMismatch\"));\n            }\n        }\n\n        return user;\n    }\n\n    /// <summary>\n    /// Tries to get the organization by the provider which is org id for us as we use the scheme\n    /// to identify organizations - not identity providers.\n    /// </summary>\n    /// <param name=\"provider\">Org id string from SSO scheme property</param>\n    /// <exception cref=\"Exception\">Errors if the provider string is not a valid org id guid or if the org cannot be found by the id.</exception>\n    private async Task<Organization> GetOrganizationByProviderAsync(string provider)\n    {\n        if (!Guid.TryParse(provider, out var organizationId))\n        {\n            // TODO: support non-org (server-wide) SSO in the future?\n            throw new Exception(_i18nService.T(\"SSOProviderIsNotAnOrgId\", provider));","sourceCodeStart":790,"sourceCodeEnd":826,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/bitwarden_license/src/Sso/Controllers/AccountController.cs#L790-L826","documentation":"Thrown during SSO manual linking when a user is found by the provided userId but the accompanying LinkSso token fails verification via UserManager.VerifyUserTokenAsync. The token is validated against the PasswordResetTokenProvider with purpose TokenPurposes.LinkSso, so failure means the token is expired, tampered with, or was issued for a different purpose/user.","triggerScenarios":"The SSO callback contains a valid userId with a token that has expired, was already consumed, belongs to a different user, or was generated before a security stamp change (e.g. password reset).","commonSituations":"The linking invitation token has a short lifespan and the user delayed completing SSO linking. A security stamp rotation (password change, account recovery) invalidated outstanding tokens. The token was copied from a different user's invitation.","solutions":["Regenerate the SSO linking invitation from the admin console to obtain a fresh LinkSso token.","Verify the token was generated with TokenPurposes.LinkSso and PasswordResetTokenProvider — a mismatched purpose will always fail verification.","If tokens expire too quickly, review the ASP.NET Core token lifespan configuration (DataProtection / TokenLifespan) for the SSO project.","Confirm the user's SecurityStamp has not changed since the token was issued."],"exampleFix":"// before: stale or wrong-purpose token\nvar token = oldTokenFromEmail;\n// after: regenerate with correct provider and purpose\nvar token = await userManager.GenerateUserTokenAsync(\n    user, TokenOptions.DefaultProvider, TokenPurposes.LinkSso);","handlingStrategy":"try-catch","validationCode":"// Before submitting SSO linking, verify the token is still valid by checking its age\nvar tokenAge = DateTime.UtcNow - tokenIssuedAt;\nif (tokenAge > TimeSpan.FromHours(24)) // typical token lifespan\n{\n    return Redirect(\"/regenerate-linking-invite\");\n}","typeGuard":null,"tryCatchPattern":"try { var user = await GetUserFromManualLinkingDataAsync(userIdentifier); }\ncatch (Exception ex) when (ex.Message.Contains(\"UserIdAndTokenMismatch\"))\n{ /* Token is invalid/expired — prompt user to request a new SSO linking invitation */ }","preventionTips":["Generate fresh LinkSso tokens immediately before the user initiates linking.","Monitor SecurityStamp changes (password resets, account recovery) that invalidate outstanding tokens.","Set a generous but finite token lifespan in TokenOptions to avoid stale-token failures.","Include the token generation timestamp in logs (not the token itself) for debugging expiry."],"tags":["sso","authentication","token","security","bitwarden-license"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}