{"record":{"id":"adf9808371e1613c","repo":"fullstackhero/dotnet-starter-kit","slug":"an-error-occurred-while-confirming-phone-number-0","errorCode":null,"errorMessage":"An error occurred while confirming phone number {0}","messagePattern":"An error occurred while confirming phone number (.+?)","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/UserRegistrationService.cs","lineNumber":154,"sourceCode":"        await SendConfirmationEmailAsync(user, origin, cancellationToken);\n    }\n\n    public async Task<string> ConfirmPhoneNumberAsync(string userId, string code, CancellationToken cancellationToken = default)\n    {\n        EnsureValidTenant();\n\n        var user = await userManager.Users\n            .Where(u => u.Id == userId && !u.PhoneNumberConfirmed)\n            .FirstOrDefaultAsync(cancellationToken);\n\n        _ = user ?? throw new CustomException(\"An error occurred while confirming phone number.\");\n\n        code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));\n        var result = await userManager.ChangePhoneNumberAsync(user, user.PhoneNumber!, code);\n\n        return result.Succeeded\n            ? string.Format(CultureInfo.InvariantCulture, \"Phone number {0} confirmed successfully.\", user.PhoneNumber)\n            : throw new CustomException(string.Format(CultureInfo.InvariantCulture, \"An error occurred while confirming phone number {0}\", user.PhoneNumber));\n    }\n\n    private void EnsureValidTenant()\n    {\n        if (string.IsNullOrWhiteSpace(multiTenantContextAccessor?.MultiTenantContext?.TenantInfo?.Id))\n        {\n            throw new UnauthorizedException(\"invalid tenant\");\n        }\n    }\n\n    private static string ExtractEmailFromPrincipal(ClaimsPrincipal principal)\n    {\n        return principal.FindFirstValue(ClaimTypes.Email)\n            ?? principal.FindFirstValue(\"email\")\n            ?? throw new CustomException(\"Email claim is required for external authentication.\");\n    }\n\n    private async Task<FshUser> CreateUserFromPrincipalAsync(ClaimsPrincipal principal, string email)","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/UserRegistrationService.cs#L136-L172","documentation":"Thrown as a CustomException when userManager.ChangePhoneNumberAsync returns a failed IdentityResult — most commonly because the supplied code is invalid, expired, or malformed after Base64Url decoding. The message includes the phone number for context. It means the code could not be verified against the generated phone-confirmation token.","triggerScenarios":"Submitting an incorrect SMS code; a code that expired (security-stamp or token lifetime changed); reusing an already-consumed code; code string mangled in transit (padding/url-encoding) so it decodes to the wrong bytes.","commonSituations":"User types the code wrong; SMS delayed and token expires; two confirmation attempts where the first consumed the token; proxies altering the Base64Url token in query strings.","solutions":["Have the user re-enter the code carefully, then request a fresh code if it fails again","Verify the code is passed unmodified (no extra URL encoding/trimming) to the API","If the security stamp changed (password reset, 2FA change), regenerate the phone confirmation token","Check clock skew and token lifetime settings if codes expire prematurely"],"exampleFix":"// before\nvar msg = await service.ConfirmPhoneNumberAsync(userId, rawCode, ct);\n// after\ntry\n{\n    var msg = await service.ConfirmPhoneNumberAsync(userId, Uri.UnescapeDataString(rawCode.Trim()), ct);\n}\ncatch (CustomException)\n{\n    await service.SendPhoneCodeAsync(userId); // fresh code\n    throw new InvalidCodeError(\"Code invalid or expired — a new code has been sent.\");\n}","handlingStrategy":"validation","validationCode":"string normalized = code?.Trim(); if (string.IsNullOrWhiteSpace(normalized) || normalized.Any(char.IsWhiteSpace)) throw new ArgumentException(\"Code must be a non-empty Base64Url string\");","typeGuard":"bool IsValidBase64Url(string s) => !string.IsNullOrWhiteSpace(s) && s.All(c => char.IsAsciiLetterOrDigit(c) || c is '-' or '_');","tryCatchPattern":"catch (CustomException ex) { await resendNewCodeAsync(userId); return Results.BadRequest(\"Code invalid or expired — a new code was sent.\"); }","preventionTips":["Pass the code through unmodified (beware double URL-encoding in query strings)","Set realistic token lifetimes and inform users of expiry","Always offer a 'resend code' path; treat failure as regenerate-and-retry"],"tags":["identity","otp","phone-confirmation","token-expired"],"backgroundTag":"invalid-argument-format","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}