{"record":{"id":"a3811ac0f19b306e","repo":"usememos/memos","slug":"unauthenticated-a3811a","errorCode":"Unauthenticated","errorMessage":"invalid token type: expected refresh token","messagePattern":"invalid token type: expected refresh token","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"server/auth/token.go","lineNumber":246,"sourceCode":"\t}\n\tif claims.Type != \"access\" {\n\t\treturn nil, errors.New(\"invalid token type: expected access token\")\n\t}\n\treturn claims, nil\n}\n\n// ParseRefreshToken parses and validates a refresh token.\nfunc ParseRefreshToken(tokenString string, secret []byte) (*RefreshTokenClaims, error) {\n\tclaims := &RefreshTokenClaims{}\n\t_, err := jwt.ParseWithClaims(tokenString, claims, verifyJWTKeyFunc(secret),\n\t\tjwt.WithIssuer(Issuer),\n\t\tjwt.WithAudience(RefreshTokenAudienceName),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif claims.Type != \"refresh\" {\n\t\treturn nil, errors.New(\"invalid token type: expected refresh token\")\n\t}\n\treturn claims, nil\n}\n","sourceCodeStart":228,"sourceCodeEnd":250,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/server/auth/token.go#L228-L250","documentation":"ParseRefreshToken accepted a JWT with valid signature, issuer, and audience, but its custom Type claim is not \"refresh\" — i.e. an access token (or other token type) was supplied where a refresh token is required. Unauthenticated.","triggerScenarios":"Sending an access token to the refresh endpoint, persisting/submitting the wrong token field from client storage, or a custom integration mixing up the two token strings returned by the login response.","commonSituations":"Client bugs that store both tokens under one key or swap them; API consumers assuming a single-token model; token payload changes after an instance upgrade while old clients cache the wrong field.","solutions":["Use the refreshToken field (not accessToken) when calling the token refresh endpoint","In client storage, keep distinct keys for access and refresh tokens and read them explicitly","Log which token type was sent when debugging; decode the JWT payload and check its `typ`/type claim equals refresh"],"exampleFix":"// before\nrefresh(cached.accessToken)\n// after\nrefresh(cached.refreshToken)","handlingStrategy":"type-guard","validationCode":"// Decode (without verifying) and check the type claim before using the token\nfunc isRefreshToken(jwtStr string) bool {\n  parts := strings.Split(jwtStr, \".\")\n  if len(parts) != 3 { return false }\n  payload, err := base64.RawURLEncoding.DecodeString(parts[1])\n  if err != nil { return false }\n  var claims struct{ Type string `json:\"type\"` }\n  if json.Unmarshal(payload, &claims) != nil { return false }\n  return claims.Type == \"refresh\"\n}","typeGuard":"func isRefreshTokenString(s string) bool {\n  p := strings.Split(s, \".\")\n  if len(p) != 3 { return false }\n  raw, err := base64.RawURLEncoding.DecodeString(p[1]); if err != nil { return false }\n  var c struct{ Type string `json:\"type\"` }\n  return json.Unmarshal(raw, &c) == nil && c.Type == \"refresh\"\n}","tryCatchPattern":"// Fail fast with an explicit message when the wrong token is supplied\nif _, err := auth.ParseRefreshToken(tok, secret); err != nil {\n  if strings.Contains(err.Error(), \"expected refresh token\") {\n    return errors.New(\"supplied an access token where a refresh token is required\")\n  }\n  return err\n}","preventionTips":["Store access and refresh tokens under distinct, clearly named keys","Name variables for their role (accessToken vs refreshToken), never generic 'token'","Add a debug claim dump when integrating new clients"],"tags":["authentication","jwt","refresh-token"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}