@@ -647,6 +647,106 @@ func (s *Server) handleRefreshTokenGrant(w http.ResponseWriter, req *TokenReques
647647 _ = json .NewEncoder (w ).Encode (resp ) //nolint:gosec // G117: OAuth token response contains access_token by spec
648648}
649649
650+ // revocationHandler handles token revocation (RFC 7009).
651+ func (s * Server ) revocationHandler () http.Handler {
652+ return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
653+ w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
654+ w .Header ().Set ("Access-Control-Allow-Methods" , "POST, OPTIONS" )
655+ w .Header ().Set ("Access-Control-Allow-Headers" , "Content-Type, Authorization" )
656+
657+ if r .Method == http .MethodOptions {
658+ w .WriteHeader (http .StatusNoContent )
659+ return
660+ }
661+
662+ if r .Method != http .MethodPost {
663+ writeOAuthError (w , http .StatusMethodNotAllowed , ErrorInvalidRequest , "Method not allowed" )
664+ return
665+ }
666+
667+ // Parse form data
668+ if err := r .ParseForm (); err != nil {
669+ writeOAuthError (w , http .StatusBadRequest , ErrorInvalidRequest , "Failed to parse request" )
670+ return
671+ }
672+
673+ token := r .Form .Get ("token" )
674+ if token == "" {
675+ writeOAuthError (w , http .StatusBadRequest , ErrorInvalidRequest , "token parameter is required" )
676+ return
677+ }
678+
679+ tokenTypeHint := r .Form .Get ("token_type_hint" )
680+
681+ // Authenticate the client (optional for public clients)
682+ clientID , clientSecret , ok := r .BasicAuth ()
683+ if ! ok {
684+ clientID = r .Form .Get ("client_id" )
685+ clientSecret = r .Form .Get ("client_secret" )
686+ }
687+
688+ // If client credentials provided, validate them
689+ if clientID != "" {
690+ client , err := s .storage .GetClient (clientID )
691+ if err != nil || client .ClientSecret != clientSecret {
692+ s .logDebugCtx (r .Context (), "revocation: invalid client credentials" ,
693+ "client_id" , clientID )
694+ writeOAuthError (w , http .StatusUnauthorized , ErrorInvalidClient , "Invalid client credentials" )
695+ return
696+ }
697+ }
698+
699+ // Try to revoke the token
700+ revoked := false
701+
702+ // Try as access token first (or if hinted)
703+ if tokenTypeHint == "" || tokenTypeHint == "access_token" {
704+ tokenInfo , err := s .storage .GetToken (token )
705+ if err == nil {
706+ // Verify client owns this token (if client authenticated)
707+ if clientID != "" && tokenInfo .ClientID != clientID {
708+ s .logDebugCtx (r .Context (), "revocation: token belongs to different client" ,
709+ "client_id" , clientID ,
710+ "token_client_id" , tokenInfo .ClientID )
711+ // Per RFC 7009, we return success even if we don't revoke
712+ w .WriteHeader (http .StatusOK )
713+ return
714+ }
715+ if err := s .storage .DeleteToken (token ); err == nil {
716+ revoked = true
717+ s .logDebugCtx (r .Context (), "revocation: access token revoked" ,
718+ "client_id" , clientID )
719+ }
720+ }
721+ }
722+
723+ // Try as refresh token if not yet revoked
724+ if ! revoked && (tokenTypeHint == "" || tokenTypeHint == "refresh_token" ) {
725+ tokenInfo , err := s .storage .GetTokenByRefresh (token )
726+ if err == nil {
727+ // Verify client owns this token (if client authenticated)
728+ if clientID != "" && tokenInfo .ClientID != clientID {
729+ s .logDebugCtx (r .Context (), "revocation: token belongs to different client" ,
730+ "client_id" , clientID ,
731+ "token_client_id" , tokenInfo .ClientID )
732+ // Per RFC 7009, we return success even if we don't revoke
733+ w .WriteHeader (http .StatusOK )
734+ return
735+ }
736+ if err := s .storage .DeleteToken (tokenInfo .AccessToken ); err == nil {
737+ s .logDebugCtx (r .Context (), "revocation: refresh token revoked" ,
738+ "client_id" , clientID )
739+ }
740+ }
741+ }
742+ _ = revoked // Silence unused variable lint
743+
744+ // Per RFC 7009, always return 200 OK (even if token wasn't found)
745+ // This prevents token enumeration attacks
746+ w .WriteHeader (http .StatusOK )
747+ })
748+ }
749+
650750// metadataHandler returns the authorization server metadata (RFC 8414).
651751func (s * Server ) metadataHandler () http.Handler {
652752 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
@@ -671,14 +771,16 @@ func (s *Server) metadataHandler() http.Handler {
671771 }
672772
673773 metadata := map [string ]interface {}{
674- "issuer" : baseURL ,
675- "authorization_endpoint" : baseURL + s .paths .Authorization ,
676- "token_endpoint" : baseURL + s .paths .Token ,
677- "registration_endpoint" : baseURL + s .paths .Registration ,
678- "response_types_supported" : []string {"code" },
679- "grant_types_supported" : []string {"authorization_code" , "refresh_token" },
680- "token_endpoint_auth_methods_supported" : []string {"none" , "client_secret_basic" , "client_secret_post" },
681- "code_challenge_methods_supported" : []string {"S256" },
774+ "issuer" : baseURL ,
775+ "authorization_endpoint" : baseURL + s .paths .Authorization ,
776+ "token_endpoint" : baseURL + s .paths .Token ,
777+ "registration_endpoint" : baseURL + s .paths .Registration ,
778+ "revocation_endpoint" : baseURL + s .paths .Revocation ,
779+ "response_types_supported" : []string {"code" },
780+ "grant_types_supported" : []string {"authorization_code" , "refresh_token" },
781+ "token_endpoint_auth_methods_supported" : []string {"none" , "client_secret_basic" , "client_secret_post" },
782+ "revocation_endpoint_auth_methods_supported" : []string {"none" , "client_secret_basic" , "client_secret_post" },
783+ "code_challenge_methods_supported" : []string {"S256" },
682784 }
683785
684786 if len (s .config .AllowedScopes ) > 0 {
0 commit comments