mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:12:25 -05:00 
			
		
		
		
	[feature] Implement /oauth/revoke for token revocation
This commit is contained in:
		
					parent
					
						
							
								b1a4d54c14
							
						
					
				
			
			
				commit
				
					
						5aa85fe760
					
				
			
		
					 8 changed files with 522 additions and 9 deletions
				
			
		|  | @ -13197,6 +13197,43 @@ paths: | |||
|             summary: Returns a compliant nodeinfo response to node info queries. | ||||
|             tags: | ||||
|                 - nodeinfo | ||||
|     /oauth/revoke: | ||||
|         post: | ||||
|             consumes: | ||||
|                 - multipart/form-data | ||||
|             operationId: oauthTokenRevoke | ||||
|             parameters: | ||||
|                 - description: The client ID, obtained during app registration. | ||||
|                   in: formData | ||||
|                   name: client_id | ||||
|                   required: true | ||||
|                   type: string | ||||
|                 - description: The client secret, obtained during app registration. | ||||
|                   in: formData | ||||
|                   name: client_secret | ||||
|                   required: true | ||||
|                   type: string | ||||
|                 - description: The previously obtained token, to be invalidated. | ||||
|                   in: formData | ||||
|                   name: token | ||||
|                   required: true | ||||
|                   type: string | ||||
|             produces: | ||||
|                 - application/json | ||||
|             responses: | ||||
|                 "200": | ||||
|                     description: OK - If you own the provided token, the API call will provide OK and an empty response `{}`. This operation is idempotent, so calling this API multiple times will still return OK. | ||||
|                 "400": | ||||
|                     description: bad request | ||||
|                 "403": | ||||
|                     description: forbidden - If you provide a token you do not own, the API call will return a 403 error. | ||||
|                 "406": | ||||
|                     description: not acceptable | ||||
|                 "500": | ||||
|                     description: internal server error | ||||
|             summary: Revoke an access token to make it no longer valid for use. | ||||
|             tags: | ||||
|                 - oauth | ||||
|     /readyz: | ||||
|         get: | ||||
|             description: If GtS is not ready, 500 Internal Error will be returned, and an error will be logged (but not returned to the caller, to avoid leaking internals). | ||||
|  |  | |||
|  | @ -46,6 +46,7 @@ const ( | |||
| 	OauthFinalizePath  = "/finalize" | ||||
| 	OauthOOBTokenPath  = "/oob"   // #nosec G101 else we get a hardcoded credentials warning | ||||
| 	OauthTokenPath     = "/token" // #nosec G101 else we get a hardcoded credentials warning | ||||
| 	OauthRevokePath    = "/revoke" | ||||
| 
 | ||||
| 	/* | ||||
| 		params / session keys | ||||
|  | @ -100,6 +101,7 @@ func (m *Module) RouteAuth(attachHandler func(method string, path string, f ...g | |||
| // RouteOAuth routes all paths that should have an 'oauth' prefix | ||||
| func (m *Module) RouteOAuth(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { | ||||
| 	attachHandler(http.MethodPost, OauthTokenPath, m.TokenPOSTHandler) | ||||
| 	attachHandler(http.MethodPost, OauthRevokePath, m.TokenRevokePOSTHandler) | ||||
| 	attachHandler(http.MethodGet, OauthAuthorizePath, m.AuthorizeGETHandler) | ||||
| 	attachHandler(http.MethodPost, OauthAuthorizePath, m.AuthorizePOSTHandler) | ||||
| 	attachHandler(http.MethodPost, OauthFinalizePath, m.FinalizePOSTHandler) | ||||
|  |  | |||
|  | @ -107,28 +107,40 @@ func (suite *AuthStandardTestSuite) TearDownTest() { | |||
| 	testrig.StopWorkers(&suite.state) | ||||
| } | ||||
| 
 | ||||
| func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string, requestBody []byte, bodyContentType string) (*gin.Context, *httptest.ResponseRecorder) { | ||||
| 	// create the recorder and gin test context | ||||
| func (suite *AuthStandardTestSuite) newContext( | ||||
| 	requestMethod string, | ||||
| 	requestPath string, | ||||
| 	requestBody []byte, | ||||
| 	bodyContentType string, | ||||
| ) (*gin.Context, *httptest.ResponseRecorder) { | ||||
| 	// Create the recorder and test context. | ||||
| 	recorder := httptest.NewRecorder() | ||||
| 	ctx, engine := testrig.CreateGinTestContext(recorder, nil) | ||||
| 
 | ||||
| 	// load templates into the engine | ||||
| 	// Load templates into the engine. | ||||
| 	testrig.ConfigureTemplatesWithGin(engine, "../../../web/template") | ||||
| 
 | ||||
| 	// create the request | ||||
| 	// Create the request itself. | ||||
| 	protocol := config.GetProtocol() | ||||
| 	host := config.GetHost() | ||||
| 	baseURI := fmt.Sprintf("%s://%s", protocol, host) | ||||
| 	requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath) | ||||
| 	ctx.Request = httptest.NewRequest( | ||||
| 		requestMethod, | ||||
| 		requestURI, | ||||
| 		bytes.NewReader(requestBody), | ||||
| 	) | ||||
| 
 | ||||
| 	ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting | ||||
| 	ctx.Request.Header.Set("accept", "text/html") | ||||
| 
 | ||||
| 	// Transmit appropriate Content-Type. | ||||
| 	if bodyContentType != "" { | ||||
| 		ctx.Request.Header.Set("Content-Type", bodyContentType) | ||||
| 	} | ||||
| 
 | ||||
| 	// trigger the session middleware on the context | ||||
| 	// Accept whatever, so we can use | ||||
| 	// this to test both HTML and JSON. | ||||
| 	ctx.Request.Header.Set("accept", "*/*") | ||||
| 
 | ||||
| 	// Trigger the session middleware on the context. | ||||
| 	store := memstore.NewStore(make([]byte, 32), make([]byte, 32)) | ||||
| 	store.Options(middleware.SessionOptions()) | ||||
| 	sessionMiddleware := sessions.Sessions("gotosocial-localhost", store) | ||||
|  |  | |||
							
								
								
									
										133
									
								
								internal/api/auth/revoke.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								internal/api/auth/revoke.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,133 @@ | |||
| // GoToSocial | ||||
| // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| package auth | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	oautherr "codeberg.org/superseriousbusiness/oauth2/v4/errors" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||
| ) | ||||
| 
 | ||||
| // TokenRevokePOSTHandler swagger:operation POST /oauth/revoke oauthTokenRevoke | ||||
| // | ||||
| // Revoke an access token to make it no longer valid for use. | ||||
| // | ||||
| //	--- | ||||
| //	tags: | ||||
| //	- oauth | ||||
| // | ||||
| //	consumes: | ||||
| //	- multipart/form-data | ||||
| // | ||||
| //	produces: | ||||
| //	- application/json | ||||
| // | ||||
| //	parameters: | ||||
| //	- | ||||
| //		name: client_id | ||||
| //		in: formData | ||||
| //		description: The client ID, obtained during app registration. | ||||
| //		type: string | ||||
| //		required: true | ||||
| //	- | ||||
| //		name: client_secret | ||||
| //		in: formData | ||||
| //		description: The client secret, obtained during app registration. | ||||
| //		type: string | ||||
| //		required: true | ||||
| //	- | ||||
| //		name: token | ||||
| //		in: formData | ||||
| //		description: The previously obtained token, to be invalidated. | ||||
| //		type: string | ||||
| //		required: true | ||||
| // | ||||
| //	responses: | ||||
| //		'200': | ||||
| //			description: >- | ||||
| //				OK - If you own the provided token, the API call will provide OK and an empty response `{}`. | ||||
| //				This operation is idempotent, so calling this API multiple times will still return OK. | ||||
| //		'400': | ||||
| //			description: bad request | ||||
| //		'403': | ||||
| //			description: >- | ||||
| //				forbidden - If you provide a token you do not own, the API call will return a 403 error. | ||||
| //		'406': | ||||
| //			description: not acceptable | ||||
| //		'500': | ||||
| //			description: internal server error | ||||
| func (m *Module) TokenRevokePOSTHandler(c *gin.Context) { | ||||
| 	if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil { | ||||
| 		apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	form := &struct { | ||||
| 		ClientID     string `form:"client_id" validate:"required"` | ||||
| 		ClientSecret string `form:"client_secret" validate:"required"` | ||||
| 		Token        string `form:"token" validate:"required"` | ||||
| 	}{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		errWithCode := gtserror.NewErrorBadRequest(err, err.Error()) | ||||
| 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if form.Token == "" { | ||||
| 		errWithCode := gtserror.NewErrorBadRequest( | ||||
| 			oautherr.ErrInvalidRequest, | ||||
| 			"token not set", | ||||
| 		) | ||||
| 		apiutil.OAuthErrorHandler(c, errWithCode) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if form.ClientID == "" { | ||||
| 		errWithCode := gtserror.NewErrorBadRequest( | ||||
| 			oautherr.ErrInvalidRequest, | ||||
| 			"client_id not set", | ||||
| 		) | ||||
| 		apiutil.OAuthErrorHandler(c, errWithCode) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if form.ClientSecret == "" { | ||||
| 		errWithCode := gtserror.NewErrorBadRequest( | ||||
| 			oautherr.ErrInvalidRequest, | ||||
| 			"client_secret not set", | ||||
| 		) | ||||
| 		apiutil.OAuthErrorHandler(c, errWithCode) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	errWithCode := m.processor.OAuthRevokeAccessToken( | ||||
| 		c.Request.Context(), | ||||
| 		form.ClientID, | ||||
| 		form.ClientSecret, | ||||
| 		form.Token, | ||||
| 	) | ||||
| 	if errWithCode != nil { | ||||
| 		apiutil.OAuthErrorHandler(c, errWithCode) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiutil.JSON(c, http.StatusOK, struct{}{}) | ||||
| } | ||||
							
								
								
									
										199
									
								
								internal/api/auth/revoke_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								internal/api/auth/revoke_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,199 @@ | |||
| // GoToSocial | ||||
| // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| package auth_test | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||
| ) | ||||
| 
 | ||||
| type RevokeTestSuite struct { | ||||
| 	AuthStandardTestSuite | ||||
| } | ||||
| 
 | ||||
| func (suite *RevokeTestSuite) TestRevokeOK() { | ||||
| 	var ( | ||||
| 		app   = suite.testApplications["application_1"] | ||||
| 		token = suite.testTokens["local_account_1"] | ||||
| 	) | ||||
| 
 | ||||
| 	// Prepare request form. | ||||
| 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||
| 		nil, | ||||
| 		map[string][]string{ | ||||
| 			"token":         {token.Access}, | ||||
| 			"client_id":     {app.ClientID}, | ||||
| 			"client_secret": {app.ClientSecret}, | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Prepare request ctx. | ||||
| 	ctx, recorder := suite.newContext( | ||||
| 		http.MethodPost, | ||||
| 		"/oauth/revoke", | ||||
| 		requestBody.Bytes(), | ||||
| 		w.FormDataContentType(), | ||||
| 	) | ||||
| 
 | ||||
| 	// Submit the revoke request. | ||||
| 	suite.authModule.TokenRevokePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// Check response code. | ||||
| 	// We don't really care about body. | ||||
| 	suite.Equal(http.StatusOK, recorder.Code) | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 
 | ||||
| 	// Ensure token now gone. | ||||
| 	_, err = suite.state.DB.GetTokenByAccess( | ||||
| 		context.Background(), | ||||
| 		token.Access, | ||||
| 	) | ||||
| 	suite.ErrorIs(err, db.ErrNoEntries) | ||||
| } | ||||
| 
 | ||||
| func (suite *RevokeTestSuite) TestRevokeWrongSecret() { | ||||
| 	var ( | ||||
| 		app   = suite.testApplications["application_1"] | ||||
| 		token = suite.testTokens["local_account_1"] | ||||
| 	) | ||||
| 
 | ||||
| 	// Prepare request form. | ||||
| 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||
| 		nil, | ||||
| 		map[string][]string{ | ||||
| 			"token":         {token.Access}, | ||||
| 			"client_id":     {app.ClientID}, | ||||
| 			"client_secret": {"Not the right secret :( :( :("}, | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Prepare request ctx. | ||||
| 	ctx, recorder := suite.newContext( | ||||
| 		http.MethodPost, | ||||
| 		"/oauth/revoke", | ||||
| 		requestBody.Bytes(), | ||||
| 		w.FormDataContentType(), | ||||
| 	) | ||||
| 
 | ||||
| 	// Submit the revoke request. | ||||
| 	suite.authModule.TokenRevokePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// Check response code + body. | ||||
| 	suite.Equal(http.StatusForbidden, recorder.Code) | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 
 | ||||
| 	// Read json bytes. | ||||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Indent nicely. | ||||
| 	dst := bytes.Buffer{} | ||||
| 	if err := json.Indent(&dst, b, "", "  "); err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	suite.Equal(`{ | ||||
|   "error": "unauthorized_client", | ||||
|   "error_description": "Forbidden: You are not authorized to revoke this token" | ||||
| }`, dst.String()) | ||||
| 
 | ||||
| 	// Ensure token still there. | ||||
| 	_, err = suite.state.DB.GetTokenByAccess( | ||||
| 		context.Background(), | ||||
| 		token.Access, | ||||
| 	) | ||||
| 	suite.NoError(err) | ||||
| } | ||||
| 
 | ||||
| func (suite *RevokeTestSuite) TestRevokeNoClientID() { | ||||
| 	var ( | ||||
| 		app   = suite.testApplications["application_1"] | ||||
| 		token = suite.testTokens["local_account_1"] | ||||
| 	) | ||||
| 
 | ||||
| 	// Prepare request form. | ||||
| 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||
| 		nil, | ||||
| 		map[string][]string{ | ||||
| 			"token":         {token.Access}, | ||||
| 			"client_secret": {app.ClientSecret}, | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Prepare request ctx. | ||||
| 	ctx, recorder := suite.newContext( | ||||
| 		http.MethodPost, | ||||
| 		"/oauth/revoke", | ||||
| 		requestBody.Bytes(), | ||||
| 		w.FormDataContentType(), | ||||
| 	) | ||||
| 
 | ||||
| 	// Submit the revoke request. | ||||
| 	suite.authModule.TokenRevokePOSTHandler(ctx) | ||||
| 
 | ||||
| 	// Check response code + body. | ||||
| 	suite.Equal(http.StatusBadRequest, recorder.Code) | ||||
| 	result := recorder.Result() | ||||
| 	defer result.Body.Close() | ||||
| 
 | ||||
| 	// Read json bytes. | ||||
| 	b, err := io.ReadAll(result.Body) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Indent nicely. | ||||
| 	dst := bytes.Buffer{} | ||||
| 	if err := json.Indent(&dst, b, "", "  "); err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	suite.Equal(`{ | ||||
|   "error": "invalid_request", | ||||
|   "error_description": "Bad Request: client_id not set" | ||||
| }`, dst.String()) | ||||
| 
 | ||||
| 	// Ensure token still there. | ||||
| 	_, err = suite.state.DB.GetTokenByAccess( | ||||
| 		context.Background(), | ||||
| 		token.Access, | ||||
| 	) | ||||
| 	suite.NoError(err) | ||||
| } | ||||
| 
 | ||||
| func TestRevokeTestSuite(t *testing.T) { | ||||
| 	suite.Run(t, new(RevokeTestSuite)) | ||||
| } | ||||
|  | @ -24,6 +24,7 @@ import ( | |||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	errorsv2 "codeberg.org/gruf/go-errors/v2" | ||||
| 	"codeberg.org/superseriousbusiness/oauth2/v4" | ||||
| 	oautherr "codeberg.org/superseriousbusiness/oauth2/v4/errors" | ||||
| 	"codeberg.org/superseriousbusiness/oauth2/v4/manage" | ||||
|  | @ -71,6 +72,7 @@ type Server interface { | |||
| 	ValidationBearerToken(r *http.Request) (oauth2.TokenInfo, error) | ||||
| 	GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, clientSecret string, userID string) (accessToken oauth2.TokenInfo, err error) | ||||
| 	LoadAccessToken(ctx context.Context, access string) (accessToken oauth2.TokenInfo, err error) | ||||
| 	RevokeAccessToken(ctx context.Context, clientID string, clientSecret string, access string) gtserror.WithCode | ||||
| } | ||||
| 
 | ||||
| // s fulfils the Server interface | ||||
|  | @ -338,3 +340,75 @@ func (s *s) GenerateUserAccessToken(ctx context.Context, ti oauth2.TokenInfo, cl | |||
| func (s *s) LoadAccessToken(ctx context.Context, access string) (accessToken oauth2.TokenInfo, err error) { | ||||
| 	return s.server.Manager.LoadAccessToken(ctx, access) | ||||
| } | ||||
| 
 | ||||
| func (s *s) RevokeAccessToken( | ||||
| 	ctx context.Context, | ||||
| 	clientID string, | ||||
| 	clientSecret string, | ||||
| 	access string, | ||||
| ) gtserror.WithCode { | ||||
| 	token, err := s.server.Manager.LoadAccessToken(ctx, access) | ||||
| 	switch { | ||||
| 	case err == nil: | ||||
| 		// Got the token, can | ||||
| 		// proceed to invalidate. | ||||
| 
 | ||||
| 	case errorsv2.IsV2( | ||||
| 		err, | ||||
| 		db.ErrNoEntries, | ||||
| 		oautherr.ErrExpiredAccessToken, | ||||
| 	): | ||||
| 		// Token already deleted, expired, | ||||
| 		// or doesn't exist, nothing to do. | ||||
| 		return nil | ||||
| 
 | ||||
| 	default: | ||||
| 		// Real error. | ||||
| 		log.Errorf(ctx, "db error loading access token: %v", err) | ||||
| 		return gtserror.NewErrorInternalError( | ||||
| 			oautherr.ErrServerError, | ||||
| 			"db error loading access token, check logs", | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	// Ensure token's client ID matches provided client ID. | ||||
| 	if token.GetClientID() != clientID { | ||||
| 		log.Debug(ctx, "client id of token does not match provided client_id") | ||||
| 		return gtserror.NewErrorForbidden( | ||||
| 			oautherr.ErrUnauthorizedClient, | ||||
| 			"You are not authorized to revoke this token", | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get client from the db using provided client ID. | ||||
| 	client, err := s.server.Manager.GetClient(ctx, clientID) | ||||
| 	if err != nil { | ||||
| 		log.Errorf(ctx, "db error loading client: %v", err) | ||||
| 		return gtserror.NewErrorInternalError( | ||||
| 			oautherr.ErrServerError, | ||||
| 			"db error loading client, check logs", | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	// Ensure requester also knows the client secret, | ||||
| 	// which confirms that they indeed created the client. | ||||
| 	if client.GetSecret() != clientSecret { | ||||
| 		log.Debug(ctx, "secret of client does not match provided client_secret") | ||||
| 		return gtserror.NewErrorForbidden( | ||||
| 			oautherr.ErrUnauthorizedClient, | ||||
| 			"You are not authorized to revoke this token", | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	// All good, invalidate the token. | ||||
| 	err = s.server.Manager.RemoveAccessToken(ctx, access) | ||||
| 	if err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||
| 		log.Errorf(ctx, "db error removing access token: %v", err) | ||||
| 		return gtserror.NewErrorInternalError( | ||||
| 			oautherr.ErrServerError, | ||||
| 			"db error removing access token, check logs", | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| package processing | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"codeberg.org/superseriousbusiness/oauth2/v4" | ||||
|  | @ -38,3 +39,17 @@ func (p *Processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, | |||
| 	// todo: some kind of metrics stuff here | ||||
| 	return p.oauthServer.ValidationBearerToken(r) | ||||
| } | ||||
| 
 | ||||
| func (p *Processor) OAuthRevokeAccessToken( | ||||
| 	ctx context.Context, | ||||
| 	clientID string, | ||||
| 	clientSecret string, | ||||
| 	accessToken string, | ||||
| ) gtserror.WithCode { | ||||
| 	return p.oauthServer.RevokeAccessToken( | ||||
| 		ctx, | ||||
| 		clientID, | ||||
| 		clientSecret, | ||||
| 		accessToken, | ||||
| 	) | ||||
| } | ||||
|  |  | |||
|  | @ -182,7 +182,48 @@ const extended = gtsApi.injectEndpoints({ | |||
| 			}, | ||||
| 		}), | ||||
| 		logout: build.mutation({ | ||||
| 			queryFn: (_arg, api) => { | ||||
| 			async queryFn(_arg, api, _extraOpts, fetchWithBQ) { | ||||
| 				const state = api.getState() as RootState; | ||||
| 				const loginState = state.login; | ||||
| 				 | ||||
| 				// Try to log out politely by revoking
 | ||||
| 				// our access token. First fetch app,
 | ||||
| 				// then token, then post to /oauth/revoke.
 | ||||
| 
 | ||||
| 				const app = loginState.app; | ||||
| 				if (app === undefined) { | ||||
| 					// This should never happen.
 | ||||
| 					throw "trying to log out with undefined app"; | ||||
| 				} | ||||
| 				 | ||||
| 				let token = loginState.token; | ||||
| 				if (token === undefined) { | ||||
| 					// This should never happen.
 | ||||
| 					throw "trying to log out with undefined token"; | ||||
| 				} | ||||
| 
 | ||||
| 				// Trim "Bearer " from stored token
 | ||||
| 				// to get just the access token part.
 | ||||
| 				token = token.substring(7); | ||||
| 
 | ||||
| 				// Try to revoke the token. If we fail, just
 | ||||
| 				// log the error and clear our state anyway.
 | ||||
| 				const invalidateResult = await fetchWithBQ({ | ||||
| 					method: "POST", | ||||
| 					url: "/oauth/revoke", | ||||
| 					body: { | ||||
| 						token: token, | ||||
| 						client_id: app.client_id, | ||||
| 						client_secret: app.client_secret, | ||||
| 					}, | ||||
| 					asForm: true, | ||||
| 				}); | ||||
| 				if (invalidateResult.error) { | ||||
| 					// eslint-disable-next-line no-console
 | ||||
| 					console.error("error logging out: ", invalidateResult.error); | ||||
| 				} | ||||
| 
 | ||||
| 				// Clear our state.
 | ||||
| 				api.dispatch(oauthRemove()); | ||||
| 				return { data: null }; | ||||
| 			}, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue