diff options
| author | 2025-02-26 13:04:55 +0100 | |
|---|---|---|
| committer | 2025-02-26 13:04:55 +0100 | |
| commit | eb720241da3d786c6ec79f2325277fa4af23846f (patch) | |
| tree | 36e0e08699e55a56d247353d082cc0a2b8144999 /internal/api/util/auth.go | |
| parent | [chore]: Bump golang.org/x/crypto from 0.33.0 to 0.34.0 (#3824) (diff) | |
| download | gotosocial-eb720241da3d786c6ec79f2325277fa4af23846f.tar.xz | |
[feature] Enforce OAuth token scopes (#3835)
* move tokenauth to apiutil
* enforce scopes
* docs
* update test models, remove deprecated "follow"
* file header
* tests
* tweak scope matcher
* simplify...
* fix tests
* log user out of settings panel in case of oauth error
Diffstat (limited to 'internal/api/util/auth.go')
| -rw-r--r-- | internal/api/util/auth.go | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/internal/api/util/auth.go b/internal/api/util/auth.go new file mode 100644 index 000000000..5c6afb306 --- /dev/null +++ b/internal/api/util/auth.go @@ -0,0 +1,152 @@ +// 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 util + +import ( + "errors" + "slices" + "strings" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/oauth2/v4" +) + +// Auth wraps an authorized token, application, user, and account. +// It is used in the functions GetAuthed and MustAuth. +// Because the user might *not* be authed, any of the fields in this struct +// might be nil, so make sure to check that when you're using this struct anywhere. +type Auth struct { + Token oauth2.TokenInfo + Application *gtsmodel.Application + User *gtsmodel.User + Account *gtsmodel.Account +} + +// TokenAuth is a convenience function for returning an TokenAuth struct from a gin context. +// In essence, it tries to extract a token, application, user, and account from the context, +// and then sets them on a struct for convenience. +// +// If any are not present in the context, they will be set to nil on the returned TokenAuth struct. +// +// If *ALL* are not present, then nil and an error will be returned. +// +// If something goes wrong during parsing, then nil and an error will be returned (consider this not authed). +// TokenAuth is like GetAuthed, but will fail if one of the requirements is not met. +func TokenAuth( + c *gin.Context, + requireToken bool, + requireApp bool, + requireUser bool, + requireAccount bool, + requireScope ...Scope, +) (*Auth, gtserror.WithCode) { + var ( + ctx = c.Copy() + a = &Auth{} + i interface{} + ok bool + ) + + i, ok = ctx.Get(oauth.SessionAuthorizedToken) + if ok { + parsed, ok := i.(oauth2.TokenInfo) + if !ok { + const errText = "could not parse token from session context" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + a.Token = parsed + } + + i, ok = ctx.Get(oauth.SessionAuthorizedApplication) + if ok { + parsed, ok := i.(*gtsmodel.Application) + if !ok { + const errText = "could not parse application from session context" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + a.Application = parsed + } + + i, ok = ctx.Get(oauth.SessionAuthorizedUser) + if ok { + parsed, ok := i.(*gtsmodel.User) + if !ok { + const errText = "could not parse user from session context" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + a.User = parsed + } + + i, ok = ctx.Get(oauth.SessionAuthorizedAccount) + if ok { + parsed, ok := i.(*gtsmodel.Account) + if !ok { + const errText = "could not parse account from session context" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + a.Account = parsed + } + + if requireToken && a.Token == nil { + const errText = "token not supplied" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + + if requireApp && a.Application == nil { + const errText = "application not supplied" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + + if requireUser && a.User == nil { + const errText = "user not supplied or not authorized" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + + if requireAccount && a.Account == nil { + const errText = "account not supplied or not authorized" + return nil, gtserror.NewErrorUnauthorized(errors.New(errText), errText) + } + + if len(requireScope) != 0 { + // We need to match one of the + // required scopes, check if we can. + hasScopes := strings.Split(a.Token.GetScope(), " ") + scopeOK := slices.ContainsFunc( + hasScopes, + func(hasScope string) bool { + for _, requiredScope := range requireScope { + if Scope(hasScope).Permits(requiredScope) { + // Got it. + return true + } + } + return false + }, + ) + + if !scopeOK { + const errText = "token has insufficient scope permission" + return nil, gtserror.NewErrorForbidden(errors.New(errText), errText) + } + } + + return a, nil +} |
