{"record":{"id":"656b14ad4c11dcb7","repo":"usememos/memos","slug":"missing-access-token-from-authorization-response","errorCode":null,"errorMessage":"missing access token from authorization response","messagePattern":"missing access token from authorization response","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"internal/idp/oauth2/oauth2.go","lineNumber":76,"sourceCode":"\t}\n\n\t// Prepare token exchange options\n\topts := []oauth2.AuthCodeOption{}\n\n\t// Add PKCE code_verifier if provided\n\tif codeVerifier != \"\" {\n\t\topts = append(opts, oauth2.SetAuthURLParam(\"code_verifier\", codeVerifier))\n\t}\n\n\ttoken, err := conf.Exchange(ctx, code, opts...)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to exchange access token\")\n\t}\n\n\t// Use the standard AccessToken field instead of Extra()\n\t// This is more reliable across different OAuth providers\n\tif token.AccessToken == \"\" {\n\t\treturn \"\", errors.New(\"missing access token from authorization response\")\n\t}\n\n\treturn token.AccessToken, nil\n}\n\n// UserInfo returns the parsed user information using the given OAuth2 token.\nfunc (p *IdentityProvider) UserInfo(ctx context.Context, token string) (*idp.IdentityProviderUserInfo, error) {\n\tclient := &http.Client{Timeout: userInfoRequestTimeout}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, p.config.UserInfoUrl, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to create http request\")\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get user information\")\n\t}","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/internal/idp/oauth2/oauth2.go#L58-L94","documentation":"After a successful OAuth2 authorization-code exchange (no error from conf.Exchange), the returned token's AccessToken field is empty. The provider accepted the exchange but issued a response without an access_token field, which the Memos IdP layer treats as a protocol violation rather than proceeding with an empty credential.","triggerScenarios":"Completing OAuth2 login where the token endpoint response omits access_token: misconfigured provider, a proxy mangling the response, custom IdP implementations returning non-standard JSON, or unusual token endpoint response_type configurations.","commonSituations":"Self-hosted custom OAuth providers (e.g. internal SSO) with non-RFC6749 token responses; wrong token URL configured in the IdP settings so a 200 HTML page is parsed as an empty token; provider version changes altering the token endpoint contract.","solutions":["Verify the IdP config in Memos: authorization URL, token URL, and client credentials","Test the token endpoint directly with curl (POST code + redirect_uri + client credentials) and confirm the JSON contains a non-empty access_token","Check the provider logs for the actual token response; fix the provider or its URL configuration","If scopes/response types were customized, return to the provider's documented defaults"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Before redirecting users, smoke-test the provider's token endpoint contract\n// (run once at IdP config time, not per login):\n// POST <tokenURL> with client credentials + a dummy code.\n// Expect 4xx from the provider, NOT a 200 without access_token — a 200 with\n// no access_token field means a misconfigured token URL or non-standard provider.\nfunc tokenEndpointLooksStandard(tokenURL, clientID, clientSecret string) bool {\n  form := url.Values{\"grant_type\": {\"authorization_code\"}, \"code\": {\"x\"}}\n  req, _ := http.NewRequest(\"POST\", tokenURL, strings.NewReader(form.Encode()))\n  req.SetBasicAuth(clientID, clientSecret)\n  req.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n  resp, err := http.DefaultClient.Do(req)\n  if err != nil { return false }\n  defer resp.Body.Close()\n  if resp.StatusCode == 200 {\n    var body map[string]any\n    json.NewDecoder(resp.Body).Decode(&body)\n    _, hasToken := body[\"access_token\"]\n    return hasToken\n  }\n  return true // proper providers reject the dummy code\n}","typeGuard":null,"tryCatchPattern":"// On IdP login failure, surface a config-oriented message and keep the session unauthenticated\nif _, err := idpSVC.Exchange(ctx, code); err != nil {\n  if strings.Contains(err.Error(), \"missing access token\") {\n    return echo.NewHTTPError(http.StatusBadGateway, \"identity provider returned an empty token; check its token endpoint configuration\")\n  }\n  return err\n}","preventionTips":["Smoke-test custom OAuth providers against the token endpoint before rollout","Keep IdP authorization/token URLs exactly as documented by the provider","Monitor token-exchange failures per provider to catch contract changes early"],"tags":["oauth2","authentication","idp"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}