{"record":{"id":"ea3cae46cd2e57be","repo":"gotify/server","slug":"the-user-s-is-already-bound-to-a-different-oidc-i","errorCode":null,"errorMessage":"the user %s is already bound to a different OIDC identity","messagePattern":"the user (.+?) is already bound to a different OIDC identity","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"api/oidc.go","lineNumber":491,"sourceCode":"\n\tbyUsername, err := a.DB.GetUserByName(username)\n\tif err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"database error: %w\", err)\n\t}\n\tif byUsername != nil {\n\t\treturn a.linkExistingUser(byUsername, oidcID, hasAdminGroup)\n\t}\n\treturn a.registerUser(username, oidcID, hasAdminGroup)\n}\n\nfunc (a *OIDCAPI) linkExistingUser(user *model.User, oidcID string, hasAdminGroup bool) (*model.User, int, error) {\n\tif !a.LinkByUsername {\n\t\tlog.Warn().Str(\"oidc_id\", oidcID).Str(\"username\", user.Name).Msgf(\"OIDC login rejected: a local user with the username already exists and %s is disabled\", config.EnvOIDCLinkByUsername)\n\t\treturn nil, http.StatusForbidden, fmt.Errorf(\"a local user with the username %s already exists and linking by username is disabled\", user.Name)\n\t}\n\tif user.OIDCID != nil {\n\t\tlog.Warn().Str(\"oidc_id\", oidcID).Str(\"bound_oidc_id\", *user.OIDCID).Str(\"username\", user.Name).Msg(\"OIDC login rejected: the username is already bound to a different OIDC identity\")\n\t\treturn nil, http.StatusForbidden, fmt.Errorf(\"the user %s is already bound to a different OIDC identity\", user.Name)\n\t}\n\tuser.OIDCID = &oidcID\n\tif len(a.GroupsAdmin) > 0 {\n\t\tuser.Admin = hasAdminGroup\n\t}\n\tif err := a.DB.UpdateUser(user); err != nil {\n\t\treturn nil, http.StatusInternalServerError, fmt.Errorf(\"failed to bind user to OIDC identity: %w\", err)\n\t}\n\tlog.Warn().Str(\"oidc_id\", oidcID).Str(\"username\", user.Name).Bool(\"admin\", user.Admin).Msg(\"OIDC link by username\")\n\treturn user, 0, nil\n}\n\nfunc (a *OIDCAPI) registerUser(username, oidcID string, hasAdminGroup bool) (*model.User, int, error) {\n\tif !a.AutoRegister {\n\t\treturn nil, http.StatusForbidden, errors.New(\"user does not exist and auto-registration is disabled\")\n\t}\n\tuser := &model.User{\n\t\tName:   username,","sourceCodeStart":473,"sourceCodeEnd":509,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L473-L509","documentation":"linkExistingUser rejects the login with HTTP 403 when the matching local user's OIDCID is already set to a different OIDC identity. Each local account may be bound to exactly one OIDC identity; binding a second identity would allow identity hijacking, so it is refused.","triggerScenarios":"The username claim matches an existing user whose OIDCID != nil and *OIDCID != the incoming oidcID — e.g. two IdP accounts share one email/username, or the IdP changed the subject identifier.","commonSituations":"IdP re-provisioned a user with a new sub; a shared mailbox/service account used by multiple IdP identities; test accounts reused across different OIDC providers; usernames recycled in Active Directory after offboarding.","solutions":["Unbind the old OIDC ID from the local account (admin action/DB) so the new identity can bind","Use a unique username claim (e.g. email or a dedicated UPN) so identities don't collide","Check whether the IdP rotated sub values and migrate mappings accordingly","Deduplicate IdP accounts that share the same username"],"exampleFix":"// before (shared mailbox claim)\nOIDC_USERNAME_CLAIM=mail\n// after (unique identity claim)\nOIDC_USERNAME_CLAIM=preferred_username","handlingStrategy":"validation","validationCode":"// pre-check whether the username is already bound to another identity\nu, _ := db.GetUserByName(username)\nif u != nil && u.OIDCID != nil && *u.OIDCID != incomingOIDCID {\n    // resolve collision before login: unbind old identity or rename\n}","typeGuard":"func isBoundToOther(u *model.User, oidcID string) bool {\n    return u.OIDCID != nil && *u.OIDCID != oidcID\n}","tryCatchPattern":"user, status, err := resolveUser(...)\nif status == http.StatusForbidden && strings.Contains(err.Error(), \"already bound to a different OIDC identity\") {\n    // trigger admin unbind flow or prompt for a distinct IdP account\n    http.Error(w, \"username bound to another identity\", http.StatusForbidden)\n    return\n}","preventionTips":["Use a unique-per-user claim (preferred_username, UPN, email) as username","Avoid shared/service mailboxes as OIDC usernames","Track IdP sub rotation and provide an admin unbind action","Deduplicate IdP accounts sharing one username"],"tags":["oidc","authorization","http-403","identity"],"backgroundTag":"oidc-identity-already-bound","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}