{"record":{"id":"4588692858f8cdbc","repo":"semaphoreui/semaphore","slug":"oidc-sign-in-failed-the-provider-returned-no-user","errorCode":null,"errorMessage":"OIDC sign-in failed: the provider returned no user ID (sub claim). Contact your administrator.","messagePattern":"OIDC sign-in failed: the provider returned no user ID \\(sub claim\\)\\. Contact your administrator\\.","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"api/login.go","lineNumber":951,"sourceCode":"\t\t\t\tclaims.emailVerified = oidcEmailVerified(userInfo, provider)\n\t\t\t}\n\t\t}\n\n\t\tclaims.username = getRandomUsername()\n\t\tif userInfo.Profile == \"\" {\n\t\t\tclaims.name = getRandomProfileName()\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tlog.Error(err.Error())\n\t\thttp.Error(w, \"OIDC sign-in failed: could not read user info from the provider. Contact your administrator.\", http.StatusBadGateway)\n\t\treturn\n\t}\n\n\tif claims.sub == \"\" {\n\t\tlog.Error(fmt.Errorf(\"oidc provider %s returned no sub claim\", pid))\n\t\thttp.Error(w, \"OIDC sign-in failed: the provider returned no user ID (sub claim). Contact your administrator.\", http.StatusBadGateway)\n\t\treturn\n\t}\n\n\tif stateData.Link {\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\n\t\tsessionUser, uErr := helpers.Store(r).GetUser(session.UserID)\n\t\tif uErr != nil {\n\t\t\tlog.Error(uErr.Error())\n\t\t\thttp.Error(w, \"Failed to link external account.\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif lErr := linkExternalIdentity(helpers.Store(r), sessionUser, db.IdentityTypeOidc, pid, claims.sub); lErr != nil {","sourceCodeStart":933,"sourceCodeEnd":969,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/login.go#L933-L969","documentation":"oidcRedirect requires a stable user identifier (sub claim) to resolve or create the local account. If claims.sub is empty after reading token/userinfo claims, it logs 'oidc provider %s returned no sub claim' and returns HTTP 502. OIDC mandates sub; its absence means a non-conformant provider or a claim mapping that dropped it.","triggerScenarios":"IdP returns an ID token/userinfo without a sub claim, claimOidcToken/claimOidcUserInfo mapping is configured to a wrong claim name so sub never gets populated, or a token from a non-conformant OAuth2 (not OIDC) endpoint.","commonSituations":"Custom/legacy IdPs that omit sub; misconfigured claim mapping in Semaphore's provider config pointing sub at a custom attribute; providers that only return email without subject.","solutions":["Check the provider config's claim mapping and ensure the subject claim is mapped from 'sub'","Decode the returned ID token (jwt.io or logs) to confirm whether sub is actually present","If the IdP omits sub, switch to a conformant IdP config/realm or file an issue with the vendor","Update provider firmware/config for non-conformant identity products"],"exampleFix":"// before\nclaims_mapping: {sub: \"user_id\"}   // IdP never emits user_id\n// after\nclaims_mapping: {sub: \"sub\"}","handlingStrategy":"validation","validationCode":"var payload map[string]any\njson.Unmarshal(idTokenBytes, &payload)\nif sub, _ := payload[\"sub\"].(string); sub == \"\" {\n    return errors.New(\"provider token has no sub claim\")\n}","typeGuard":"func hasSub(claims map[string]any) bool {\n    s, ok := claims[\"sub\"].(string)\n    return ok && s != \"\"\n}","tryCatchPattern":null,"preventionTips":["Verify provider conformance (sub is mandatory in OIDC)","Keep claim mapping for sub pointed at 'sub'","Test a full login against the IdP before go-live"],"tags":["oidc","http-502","claims"],"backgroundTag":"empty-required-field","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"}