Adding token revocation support
1 files changed, 123 insertions(+), 20 deletions(-)

M routes.go
M routes.go +123 -20
@@ 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
 	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.C
 	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.C
 	}
 
 	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)
 }