From 5cbb45a51706a430f30cd28b87573737fc571023 Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Mon, 18 Dec 2023 17:44:45 -0600 Subject: [PATCH] Adding token revocation support --- routes.go | 143 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 20 deletions(-) diff --git a/routes.go b/routes.go index f63f50b..215ae70 100644 --- a/routes.go +++ b/routes.go @@ -40,6 +40,7 @@ type Service struct { func (s *Service) RegisterRoutes() { s.eg.POST("/access-token", s.AccessTokenPOST).Name = s.RouteName("access_token_post") s.eg.POST("/introspect", s.IntrospectPOST).Name = s.RouteName("introspect_post") + s.eg.POST("/revoke", s.RevokeTokenPOST).Name = s.RouteName("revoke_token_post") s.eg.Use(auth.AuthRequired()) s.eg.GET("/personal", s.ListPersonal).Name = s.RouteName("list_personal") @@ -735,6 +736,102 @@ func (s *Service) AccessTokenPOST(c echo.Context) error { return c.JSON(http.StatusOK, &ret) } +// RevokeTokenPOST ... +// https://datatracker.ietf.org/doc/html/rfc7009 +func (s *Service) RevokeTokenPOST(c echo.Context) error { + req := c.Request() + ctype := req.Header.Get("Content-Type") + if ctype != "application/x-www-form-urlencoded" { + return s.accessTokenError(c, "invalid_request", + "Content-Type must be application/x-www-form-urlencoded", 400) + } + + header := req.Header.Get("Authorization") + if header == "" { + return s.accessTokenError(c, "invalid_request", + "Invalid Authorization header", 400) + } + z := strings.SplitN(header, " ", 2) + if len(z) != 2 { + return s.accessTokenError(c, "invalid_request", + "Invalid Authorization header", 400) + } + if strings.ToLower(z[0]) != "basic" { + return s.accessTokenError(c, "invalid_request", + "Invalid Authorization header", 400) + } + idsec, err := base64.StdEncoding.DecodeString(z[1]) + if err != nil { + return s.accessTokenError(c, "invalid_request", + "Invalid Authorization header", 400) + } + z = strings.SplitN(string(idsec), ":", 2) + if len(z) != 2 { + return s.accessTokenError(c, "invalid_request", + "Invalid Authorization header", 400) + } + clientID, clientSecret := z[0], z[1] + client, err := GetClientByID(c.Request().Context(), clientID) + if err != nil { + c.Response().Header().Set("WWW-Authenticate", "Basic") + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization client id", 401) + } + if !client.VerifyClientSecret(clientSecret) { + c.Response().Header().Set("WWW-Authenticate", "Basic") + return s.accessTokenError(c, "invalid_client", + "Invalid Authorization client secret", 401) + } + + params, err := c.FormParams() + if err != nil { + return err + } + token := params.Get("token") + tokenHint := params.Get("token_type_hint") + if tokenHint != "" && tokenHint != "refresh_token" && tokenHint != "access_token" { + return s.accessTokenError(c, "unsupported_token_type", + "Invalid token type given", 400) + } + + if token == "" { + // For invalid token simply return 200 OK. See RFC spec + return nil + } + + hash := sha512.Sum512([]byte(token)) + hashStr := hex.EncodeToString(hash[:]) + tokenVar := "token_hash" + if tokenHint == "refresh_token" { + tokenVar = "refresh_token_hash" + } + opts := &database.FilterOptions{ + Filter: sq.And{ + sq.Eq{tokenVar: hashStr}, + sq.Eq{"client_id": client.ID}, + sq.Expr("expires at time zone 'UTC' > NOW() at time zone 'UTC'"), + }, + } + grants, err := GetGrants(c.Request().Context(), opts) + if err != nil { + c.Response().Header().Set("Retry-After", "300") + c.Response().WriteHeader(503) + return nil + } + if len(grants) == 0 { + return nil + } + grant := grants[0] + err = grant.Revoke(c.Request().Context()) + if err != nil { + c.Response().Header().Set("Retry-After", "300") + c.Response().WriteHeader(503) + return nil + } + + return nil +} + // IntrospectPOST ... func (s *Service) IntrospectPOST(c echo.Context) error { req := c.Request() @@ -793,6 +890,10 @@ func (s *Service) OAuthMetadata(c echo.Context) error { if err != nil { return err } + rURL, err := url.JoinPath(origin, c.Echo().Reverse(s.RouteName("revoke_token_post"))) + if err != nil { + return err + } var scopes []string for _, scope := range s.config.Scopes { @@ -813,27 +914,29 @@ func (s *Service) OAuthMetadata(c echo.Context) error { } ret := struct { - Issuer string `json:"issuer"` - AuthEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - Scopes []string `json:"scopes_supported"` - Responses []string `json:"response_types_supported"` - Grants []string `json:"grant_types_supported"` - Doc string `json:"service_documentation"` - IntroEndpoint string `json:"introspection_endpoint"` - IntroAuth []string `json:"introspection_endpoint_auth_methods_supported"` - ISS bool `json:"authorization_response_iss_parameter_supported"` + Issuer string `json:"issuer"` + AuthEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + RevokeEndPoint string `json:"revocation_endpoint"` + Scopes []string `json:"scopes_supported"` + Responses []string `json:"response_types_supported"` + Grants []string `json:"grant_types_supported"` + Doc string `json:"service_documentation"` + IntroEndpoint string `json:"introspection_endpoint"` + IntroAuth []string `json:"introspection_endpoint_auth_methods_supported"` + ISS bool `json:"authorization_response_iss_parameter_supported"` }{ - Issuer: origin, - AuthEndpoint: aURL, - TokenEndpoint: tURL, - Scopes: scopes, - Responses: []string{"code"}, - Grants: []string{"authorization_code", "refresh_token"}, - Doc: s.config.DocumentationURL, - IntroEndpoint: iURL, - IntroAuth: []string{"none"}, - ISS: true, + Issuer: origin, + AuthEndpoint: aURL, + TokenEndpoint: tURL, + RevokeEndPoint: rURL, + Scopes: scopes, + Responses: []string{"code"}, + Grants: []string{"authorization_code", "refresh_token"}, + Doc: s.config.DocumentationURL, + IntroEndpoint: iURL, + IntroAuth: []string{"none"}, + ISS: true, } return c.JSON(http.StatusOK, &ret) } -- 2.45.2