@@ 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)
}