~netlandish/gobwebs-oauth2

5cbb45a51706a430f30cd28b87573737fc571023 — Peter Sanchez 11 months ago 5843643
Adding token revocation support
1 files changed, 123 insertions(+), 20 deletions(-)

M routes.go
M routes.go => 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.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)
}