{"record":{"id":"333167daa8ff41ab","repo":"usememos/memos","slug":"codeunauthenticated","errorCode":"CodeUnauthenticated","errorMessage":"authentication required","messagePattern":"authentication required","errorType":"error_code","errorClass":"ErrUnauthenticated","httpStatus":401,"severity":"error","filePath":"server/router/api/v1/authz.go","lineNumber":15,"sourceCode":"package v1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/usememos/memos/internal/profile\"\n\t\"github.com/usememos/memos/server/auth\"\n\t\"github.com/usememos/memos/store\"\n)\n\n// ErrUnauthenticated is returned by the Authorizer when a request must be rejected\n// for lack of valid credentials. Each transport maps it to its own status code\n// (Connect: CodeUnauthenticated, gRPC-Gateway: HTTP 401).\nvar ErrUnauthenticated = errors.New(\"authentication required\")\n\n// Authorizer is the single source of truth for method-level access control.\n//\n// It authenticates a request from its Authorization header and decides whether the\n// (possibly anonymous) caller may reach a given RPC procedure. The Connect\n// interceptor and the gRPC-Gateway middleware share one Authorizer so both\n// transports enforce identical rules.\n//\n// Role-based authorization (admin checks) stays in the service layer; this type\n// governs only authentication and anonymous access.\ntype Authorizer struct {\n\tauthenticator *auth.Authenticator\n\tprofile       *profile.Profile\n}\n\n// NewAuthorizer creates an Authorizer backed by the given store, token secret, and\n// instance profile.\nfunc NewAuthorizer(store *store.Store, secret string, profile *profile.Profile) *Authorizer {","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/server/router/api/v1/authz.go#L1-L33","documentation":"ErrUnauthenticated is the sentinel error returned by the APIV1Service Authorizer when a request carries no valid credentials (missing, malformed, or expired Authorization header) and the target RPC does not allow anonymous access. The Connect interceptor maps it to CodeUnauthenticated and the gRPC-Gateway middleware maps it to HTTP 401, so both transports behave identically. It is thrown before any service logic runs.","triggerScenarios":"Calling any non-public RPC (e.g. MemoService.ListMemos, UserService.GetUser) without an Authorization header; sending an expired access token; sending a token with an unsupported scheme (the authenticator only accepts its defined schemes); or hitting a route not listed as anonymously accessible in acl_config.go.","commonSituations":"Frontend forgot to attach the access token via the Connect client interceptor; token expired and the refresh flow did not run (refresh token also expired or revoked); CLI/script integration using raw HTTP without Bearer auth; testing a locally protected route with curl.","solutions":["Attach a valid access token: Authorization: Bearer <access_token> obtained from SignIn/SignUp.","If the token expired, call the refresh endpoint to get a new access token before retrying.","If the endpoint should be reachable without login (e.g. GetInstanceStatus for the sign-in page), verify it is registered as an anonymous-allowed method in server/router/api/v1/acl_config.go.","Check that the token value was not truncated or prefixed with extra characters when copied."],"exampleFix":"// before\ncurl http://localhost:5230/memos.api.v1.MemoService/ListMemos \\\n  -H 'Content-Type: application/json' -d '{}'\n\n// after\ncurl http://localhost:5230/memos.api.v1.MemoService/ListMemos \\\n  -H 'Content-Type: application/json' \\\n  -H \"Authorization: Bearer $ACCESS_TOKEN\" -d '{}'","handlingStrategy":"try-catch","validationCode":"// Ensure a token exists before calling a protected RPC\nif (!accessToken) {\n  throw new Error('Sign in required before calling protected APIs');\n}","typeGuard":null,"tryCatchPattern":"try {\n  const resp = await client.listMemos({});\n} catch (e) {\n  if (e.code === 'unauthenticated') {\n    await signOut(); // clear stale token state, redirect to sign-in\n    return;\n  }\n  throw e;\n}","preventionTips":["Always create Connect clients through the shared auth interceptor that attaches and refreshes the access token.","Register the 401/unauthenticated handler to trigger a single token refresh then one retry, not an infinite loop.","Keep truly public endpoints listed in acl_config.go so unauthenticated flows (sign-in page) never hit this error."],"tags":["auth","connect-rpc","http-401","middleware"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}