8 changed files with 522 additions and 9 deletions
@ -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{}{}) |
||||
} |
||||
@ -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)) |
||||
} |
||||
Loading…
Reference in new issue