RevokeToken implements RFC 7009 OAuth2 Token Revocation Authentication is unique for this endpoint in that it does not use the standard token authentication middleware. Instead, it expects the token that is being revoked to be valid. TODO: Currently the token validation occurs in the revocation logi
(db database.Store, logger slog.Logger)
| 56 | // This code should be refactored to share token validation logic with other parts |
| 57 | // of the OAuth2 provider/http middleware. |
| 58 | func RevokeToken(db database.Store, logger slog.Logger) http.HandlerFunc { |
| 59 | return func(rw http.ResponseWriter, r *http.Request) { |
| 60 | ctx := r.Context() |
| 61 | app := httpmw.OAuth2ProviderApp(r) |
| 62 | |
| 63 | // RFC 7009 requires POST method with application/x-www-form-urlencoded |
| 64 | if r.Method != http.MethodPost { |
| 65 | httpapi.WriteOAuth2Error(ctx, rw, http.StatusMethodNotAllowed, codersdk.OAuth2ErrorCodeInvalidRequest, "Method not allowed") |
| 66 | return |
| 67 | } |
| 68 | |
| 69 | req, err := extractRevocationRequest(r) |
| 70 | if err != nil { |
| 71 | httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, err.Error()) |
| 72 | return |
| 73 | } |
| 74 | |
| 75 | // Determine if this is a refresh token (starts with "coder_") or API key |
| 76 | // APIKeys do not have the SecretIdentifier prefix. |
| 77 | const coderPrefix = SecretIdentifier + "_" |
| 78 | isRefreshToken := strings.HasPrefix(req.Token, coderPrefix) |
| 79 | |
| 80 | // Revoke the token with ownership verification |
| 81 | err = db.InTx(func(tx database.Store) error { |
| 82 | if isRefreshToken { |
| 83 | // Handle refresh token revocation |
| 84 | return revokeRefreshTokenInTx(ctx, tx, req.Token, app.ID) |
| 85 | } |
| 86 | // Handle API key revocation |
| 87 | return revokeAPIKeyInTx(ctx, tx, req.Token, app.ID) |
| 88 | }, nil) |
| 89 | if err != nil { |
| 90 | if errors.Is(err, ErrTokenNotBelongsToClient) { |
| 91 | // RFC 7009: Return success even if token doesn't belong to client (don't reveal token existence) |
| 92 | logger.Debug(ctx, "token revocation failed: token does not belong to requesting client", |
| 93 | slog.F("client_id", app.ID.String()), |
| 94 | slog.F("app_name", app.Name)) |
| 95 | rw.WriteHeader(http.StatusOK) |
| 96 | return |
| 97 | } |
| 98 | if errors.Is(err, ErrInvalidTokenFormat) { |
| 99 | // Invalid token format should return 400 bad request |
| 100 | logger.Debug(ctx, "token revocation failed: invalid token format", |
| 101 | slog.F("client_id", app.ID.String()), |
| 102 | slog.F("app_name", app.Name)) |
| 103 | httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, codersdk.OAuth2ErrorCodeInvalidRequest, "Invalid token format") |
| 104 | return |
| 105 | } |
| 106 | logger.Error(ctx, "token revocation failed with internal server error", |
| 107 | slog.Error(err), |
| 108 | slog.F("client_id", app.ID.String()), |
| 109 | slog.F("app_name", app.Name)) |
| 110 | httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, codersdk.OAuth2ErrorCodeServerError, "Internal server error") |
| 111 | return |
| 112 | } |
| 113 | |
| 114 | // RFC 7009: successful revocation returns HTTP 200 |
| 115 | rw.WriteHeader(http.StatusOK) |
no test coverage detected