{"record":{"id":"73ade7c6bb38abdd","repo":"semaphoreui/semaphore","slug":"you-must-be-signed-in-to-link-an-external-account","errorCode":null,"errorMessage":"You must be signed in to link an external account.","messagePattern":"You must be signed in to link an external account\\.","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"api/login.go","lineNumber":586,"sourceCode":"\t\tlog.Error(fmt.Errorf(\"no such provider: %s\", pid))\n\t\thttp.Error(w, \"Unknown OIDC provider.\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlinkMode := r.URL.Query().Get(\"link\") != \"\"\n\n\tif linkMode {\n\t\t// POST-only: SameSite=Lax attaches the session cookie to top-level\n\t\t// cross-site GET navigations, so a GET here would let an attacker\n\t\t// initiate linking (CSRF) and attach their IdP identity to the\n\t\t// victim's account. Lax never sends the cookie on cross-site POST.\n\t\tif r.Method != http.MethodPost {\n\t\t\thttp.Error(w, \"Account linking must be initiated with a POST request.\", http.StatusMethodNotAllowed)\n\t\t\treturn\n\t\t}\n\t\tsession, ok := getSession(r)\n\t\tif !ok || !session.IsVerified() {\n\t\t\thttp.Error(w, \"You must be signed in to link an external account.\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\n\treturnValue := r.URL.Query().Get(\"return\")\n\tif returnValue != \"\" {\n\t\tif config.ReturnViaState {\n\t\t\treturnPath = returnValue\n\t\t} else {\n\t\t\tredirectPath = returnValue\n\t\t}\n\t}\n\n\t_, oauth, err := getOidcProvider(pid, ctx, redirectPath)\n\tif err != nil {\n\t\tlog.Error(err.Error())\n\t\thttp.Error(w, \"Failed to initialize OIDC provider. Contact your administrator.\", http.StatusInternalServerError)\n\t\treturn","sourceCodeStart":568,"sourceCodeEnd":604,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/login.go#L568-L604","documentation":"In account-linking mode (?link=...), after passing the POST-method check the handler loads the user's session via getSession(r) and requires it to exist and be verified (session.IsVerified()). If there is no valid verified session, it returns HTTP 401 'You must be signed in to link an external account.' Linking an external identity requires an already-authenticated Semaphore account.","triggerScenarios":"POST /api/auth/oidc/{pid}?link=true without a session cookie; with an expired/invalidated session cookie; or with a session that is not verified (e.g. created but not yet passed verification/MFA step).","commonSituations":"User's session expired before clicking 'Link account'; session cookie blocked or stripped (same-site/cross-origin fetch without credentials); MFA not completed so the session is unverified; user attempting to link without ever signing in.","solutions":["Sign in to Semaphore (completing any MFA/verification) before initiating account linking","Ensure the linking POST request carries the session cookie (credentials: 'include' in cross-origin fetch)","Re-authenticate if the session expired, then retry the linking flow","Use the same browser/site context so SameSite cookie rules do not strip the session cookie"],"exampleFix":"// before: cookie not sent, 401\nfetch('/api/auth/oidc/github?link=true', { method: 'POST' })\n// after\nfetch('/api/auth/oidc/github?link=true', { method: 'POST', credentials: 'include' })","handlingStrategy":"validation","validationCode":"// check the user has a verified session before offering the link action:\nconst res = await fetch('/api/session/status', { credentials: 'include' })\nif (!res.ok) { /* redirect to login first; linking requires a verified session */ }","typeGuard":null,"tryCatchPattern":"const resp = await fetch(`/api/auth/oidc/${pid}?link=true`, { method: 'POST', credentials: 'include' })\nif (resp.status === 401) {\n    // send user through normal sign-in (incl. MFA), then retry linking\n}","preventionTips":["Require sign-in (and MFA verification) before rendering the 'Link account' UI","Always send credentials: 'include' on same-site session-authenticated calls","Handle session expiry by re-authenticating, then resuming the link flow","Keep users on the same site origin so the session cookie is attached"],"tags":["http","oidc","session","authentication"],"backgroundTag":"authentication-required","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}