{"record":{"id":"cad68cc552113106","repo":"gotify/server","slug":"client-not-found-cad68c","errorCode":null,"errorMessage":"client not found","messagePattern":"client not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"api/oidc.go","lineNumber":264,"sourceCode":"\t\t}\n\t\tauth.SetCookie(w, client.Token, auth.CookieMaxAge, a.SecureCookie)\n\t\t// A reverse proxy may have already stripped a url prefix from the URL\n\t\t// without us knowing, we have to make a relative redirect.\n\t\t// We cannot use http.Redirect as this normalizes the Path with r.URL.\n\t\tw.Header().Set(\"Location\", \"../../\")\n\t\tw.WriteHeader(http.StatusTemporaryRedirect)\n\t}\n\treturn gin.WrapF(rp.CodeExchangeHandler(rp.UserinfoCallback(callback), a.Provider))\n}\n\nfunc (a *OIDCAPI) handleElevationCallback(w http.ResponseWriter, elevate *pendingElevation, user *model.User) {\n\tclient, err := a.DB.GetClientByID(elevate.ClientID)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"database error: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tif client == nil || client.UserID != user.ID {\n\t\thttp.Error(w, \"client not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\televatedUntil := time.Now().Add(time.Duration(elevate.DurationSeconds) * time.Second)\n\tif err := a.DB.UpdateClientElevatedUntil(client.ID, &elevatedUntil); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"failed to elevate session: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// The UI rechecks the authentication when the tab is closed.\n\tw.WriteHeader(http.StatusOK)\n\tw.Header().Add(\"content-type\", \"text/html\")\n\tio.WriteString(w, `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Gotify Session Elevation</title>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n</head>","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L246-L282","documentation":"handleElevationCallback (api/oidc.go:264) returns HTTP 404 'client not found' when GetClientByID returns no client for elevate.ClientID, or the client exists but belongs to a different user (client.UserID != user.ID). This is a deliberate ownership/authorization guard, not a database failure.","triggerScenarios":"During the OIDC elevation callback: (1) the client was deleted between starting the elevation (ElevateHandler) and completing the provider round-trip; (2) the elevation state references a client owned by another user, e.g. after the state was crafted/replayed for a different account; (3) a stale elevation request referencing an ID from a different database/environment; (4) wrong ClientID stored in the pending elevation.","commonSituations":"User deletes the client (or an admin purges it) while the elevation tab is open at the provider; replaying an old elevation URL after re-authenticating as a different user; pointing gotify at a restored/migrated database missing that client row.","solutions":["Verify the client ID still exists and is owned by the logged-in user, then restart the elevation flow from the UI","If the client was deleted, create it again and elevate the new client","Do not reuse or share elevation URLs; each is bound to one user and one client","Check that the same database is being used across restarts/replicas","Confirm the elevation was initiated for the account you actually authenticated with at the provider"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before starting the elevation flow, confirm the client exists and is yours\nclient, err := db.GetClientByID(clientID)\nif err != nil {\n    return err // DB problem, different failure mode\n}\nif client == nil || client.UserID != currentUser.ID {\n    // do not start the elevation; it is guaranteed to 404 at the callback\n    return errors.New(\"client missing or not owned by current user\")\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch(elevateCallbackURL);\nif (res.status === 404 && (await res.text()).includes('client not found')) {\n  // client was deleted or belongs to another user: refresh client list,\n  // re-create if needed, then start a NEW elevation flow\n  await refreshClients();\n  return startElevation();\n}","preventionTips":["Verify the client still exists right before initiating elevation","Never share or replay elevation URLs across users or sessions","Refresh the client list after deletions so the UI never elevates a stale ID","Ensure all instances point at the same database","Log the ClientID and UserID on this 404 to diagnose ownership mismatches"],"tags":["oidc","authorization","not-found","client-ownership","http-404"],"backgroundTag":"resource-not-found-or-forbidden","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"}