{"record":{"id":"826edb772a63c082","repo":"usememos/memos","slug":"failed-to-link-account-please-sign-in-to-memos-ag","errorCode":null,"errorMessage":"Failed to link account. Please sign in to Memos again and retry.","messagePattern":"Failed to link account\\. Please sign in to Memos again and retry\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/pages/AuthCallback.tsx","lineNumber":87,"sourceCode":"    // Validate OAuth state (CSRF protection) and retrieve PKCE code_verifier\n    const validatedState = validateOAuthState(state);\n    if (!validatedState) {\n      setState({\n        loading: false,\n        errorMessage: \"Failed to authorize. Invalid or expired state parameter. This may indicate a CSRF attack attempt.\",\n      });\n      return;\n    }\n\n    const { flowMode, identityProviderName, returnUrl, linkingUserName, codeVerifier } = validatedState;\n    const redirectUri = absolutifyLink(\"/auth/callback\");\n    handledRef.current = true;\n\n    (async () => {\n      try {\n        if (flowMode === \"link\") {\n          if (!currentUser?.name) {\n            throw new Error(\"Failed to link account. Please sign in to Memos again and retry.\");\n          }\n          if (linkingUserName && currentUser.name !== linkingUserName) {\n            throw new Error(\"The signed-in user changed before the OAuth callback completed. Please retry linking from account settings.\");\n          }\n          await userServiceClient.createLinkedIdentity({\n            parent: currentUser.name,\n            idpName: identityProviderName,\n            code,\n            redirectUri,\n            codeVerifier: codeVerifier || \"\",\n          });\n        } else {\n          const response = await authServiceClient.signIn({\n            credentials: {\n              case: \"ssoCredentials\",\n              value: {\n                idpName: identityProviderName,\n                code,","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/web/src/pages/AuthCallback.tsx#L69-L105","documentation":"Thrown in AuthCallback's link flow when the OAuth provider redirected back with flowMode === \"link\" but currentUser?.name is falsy. Linking an identity requires an authenticated Memos session (the parent resource for createLinkedIdentity); if the session expired or tokens were lost during the round-trip to the IdP, there is no parent user to attach the identity to.","triggerScenarios":"User starts 'link account' from settings, gets redirected to the IdP, and meanwhile the Memos access/refresh token expires or auth-state storage is cleared; on callback, currentUser is null/undefined while stored state says flowMode === \"link\". Also occurs when the callback opens in a different browser profile or the auth BroadcastChannel state was reset.","commonSituations":"Long IdP login pages (password manager, 2FA) outlasting the Memos session; refresh token revoked; tokens stored per-tab and callback lands in a new tab; clock skew expiring the access token early.","solutions":["Sign in to Memos again in the same browser, then re-run the linking flow from account settings.","Verify the Memos session survived: check web/src/auth-state.ts token storage and that the auth interceptor can refresh the access token.","Ensure the OAuth redirect lands in the same browser profile/tab context where linking started (no 'open in new profile' links).","If it recurs, inspect DevTools > Application for the auth tokens and confirm the IdP redirect_uri matches the origin."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// before starting a link flow\nif (!currentUser?.name) {\n  showError(\"Sign in to Memos before linking an account.\");\n  return;\n}\n// embed the user into the state so the callback can verify\nstartLinkFlow(currentUser.name);","typeGuard":"const canLink = (user: { name?: string } | null | undefined): user is { name: string } =>\n  typeof user?.name === \"string\" && user.name.length > 0;","tryCatchPattern":"catch (e) {\n  if (e instanceof Error && e.message.includes(\"Failed to link account\")) {\n    await signOut(); // clear partial state\n    navigate(\"/auth\", { state: { reason: \"relink\" } });\n  }\n}","preventionTips":["Require an authenticated session before exposing the 'link account' action.","Store linkingUserName in OAuth state (the code already does) and verify it at callback.","Keep the Memos tab open and active for the whole IdP round-trip."],"tags":["oauth","auth","account-linking","frontend","session"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}