2023-03-12 16:00:57 +01:00
// 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/>.
2021-04-01 20:46:45 +02:00
package oauth
import (
"context"
2023-02-28 11:38:34 +01:00
"errors"
2021-04-01 20:46:45 +02:00
"fmt"
"net/http"
2022-10-08 13:49:56 +02:00
"strings"
2021-04-01 20:46:45 +02:00
2025-03-02 16:42:51 +01:00
"codeberg.org/superseriousbusiness/oauth2/v4"
oautherr "codeberg.org/superseriousbusiness/oauth2/v4/errors"
"codeberg.org/superseriousbusiness/oauth2/v4/manage"
"codeberg.org/superseriousbusiness/oauth2/v4/server"
2021-04-01 20:46:45 +02:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-06-11 10:39:39 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2025-03-03 16:03:36 +01:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-07-19 10:47:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2025-03-03 16:03:36 +01:00
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
2021-04-01 20:46:45 +02:00
)
const (
2021-04-19 19:42:19 +02:00
// SessionAuthorizedToken is the key set in the gin context for the Token
// of a User who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as oauth2.TokenInfo
2021-04-01 20:46:45 +02:00
SessionAuthorizedToken = "authorized_token"
// SessionAuthorizedUser is the key set in the gin context for the id of
// a User who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as a *gtsmodel.User
SessionAuthorizedUser = "authorized_user"
// SessionAuthorizedAccount is the key set in the gin context for the Account
// of a User who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as a *gtsmodel.Account
SessionAuthorizedAccount = "authorized_account"
2021-04-20 18:14:23 +02:00
// SessionAuthorizedApplication is the key set in the gin context for the Application
2021-04-01 20:46:45 +02:00
// of a Client who has successfully passed Bearer token authorization.
// The interface returned from grabbing this key should be parsed as a *gtsmodel.Application
SessionAuthorizedApplication = "authorized_app"
2022-10-08 13:49:56 +02:00
// OOBURI is the out-of-band oauth token uri
OOBURI = "urn:ietf:wg:oauth:2.0:oob"
// OOBTokenPath is the path to redirect out-of-band token requests to.
2023-02-18 16:47:42 +01:00
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
2022-10-08 13:49:56 +02:00
// HelpfulAdvice is a handy hint to users;
// particularly important during the login flow
2023-12-27 11:23:52 +01:00
HelpfulAdvice = "If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"
HelpfulAdviceGrant = "If you arrived at this error during a sign in/oauth flow, your client is trying to use an unsupported OAuth grant type. Supported grant types are: authorization_code, client_credentials; please reach out to developer of your client"
2021-04-01 20:46:45 +02:00
)
2025-03-03 16:03:36 +01:00
// Server wraps some oauth2 server functions
// in an interface, exposing only what is needed.
2021-04-01 20:46:45 +02:00
type Server interface {
2022-06-11 10:39:39 +02:00
HandleTokenRequest ( r * http . Request ) ( map [ string ] interface { } , gtserror . WithCode )
2022-10-08 13:49:56 +02:00
HandleAuthorizeRequest ( w http . ResponseWriter , r * http . Request ) gtserror . WithCode
2021-04-01 20:46:45 +02:00
ValidationBearerToken ( r * http . Request ) ( oauth2 . TokenInfo , error )
2021-10-04 15:24:19 +02:00
GenerateUserAccessToken ( ctx context . Context , ti oauth2 . TokenInfo , clientSecret string , userID string ) ( accessToken oauth2 . TokenInfo , err error )
2021-06-19 11:18:55 +02:00
LoadAccessToken ( ctx context . Context , access string ) ( accessToken oauth2 . TokenInfo , err error )
2021-04-01 20:46:45 +02:00
}
2025-03-03 16:03:36 +01:00
// s fulfils the Server interface
// using the underlying oauth2 server.
2021-04-01 20:46:45 +02:00
type s struct {
server * server . Server
}
2021-05-08 14:25:55 +02:00
// New returns a new oauth server that implements the Server interface
2025-03-03 16:03:36 +01:00
func New (
ctx context . Context ,
state * state . State ,
validateURIHandler manage . ValidateURIHandler ,
clientScopeHandler server . ClientScopeHandler ,
authorizeScopeHandler server . AuthorizeScopeHandler ,
internalErrorHandler server . InternalErrorHandler ,
responseErrorHandler server . ResponseErrorHandler ,
userAuthorizationHandler server . UserAuthorizationHandler ,
) Server {
ts := newTokenStore ( ctx , state )
cs := NewClientStore ( state )
// Set up OAuth2 manager.
2021-05-08 14:25:55 +02:00
manager := manage . NewDefaultManager ( )
2025-03-03 16:03:36 +01:00
manager . SetValidateURIHandler ( validateURIHandler )
2021-05-08 14:25:55 +02:00
manager . MapTokenStorage ( ts )
manager . MapClientStorage ( cs )
2025-03-03 16:03:36 +01:00
manager . SetAuthorizeCodeTokenCfg (
& manage . Config {
// Following the Mastodon API,
// access tokens don't expire.
AccessTokenExp : 0 ,
// Don't use refresh tokens.
IsGenerateRefresh : false ,
2021-05-08 14:25:55 +02:00
} ,
2025-03-03 16:03:36 +01:00
)
// Set up OAuth2 server.
srv := server . NewServer (
& server . Config {
TokenType : "Bearer" ,
// Must follow the spec.
AllowGetAccessRequest : false ,
// Support only the non-implicit flow.
AllowedResponseTypes : [ ] oauth2 . ResponseType { oauth2 . Code } ,
// Allow:
// - Authorization Code (for first & third parties)
// - Client Credentials (for applications)
AllowedGrantTypes : [ ] oauth2 . GrantType {
oauth2 . AuthorizationCode ,
oauth2 . ClientCredentials ,
} ,
AllowedCodeChallengeMethods : [ ] oauth2 . CodeChallengeMethod {
oauth2 . CodeChallengePlain ,
oauth2 . CodeChallengeS256 ,
} ,
2023-09-28 11:21:19 +02:00
} ,
2025-03-03 16:03:36 +01:00
manager ,
)
srv . SetAuthorizeScopeHandler ( authorizeScopeHandler )
srv . SetClientScopeHandler ( clientScopeHandler )
srv . SetInternalErrorHandler ( internalErrorHandler )
srv . SetResponseErrorHandler ( responseErrorHandler )
srv . SetUserAuthorizationHandler ( userAuthorizationHandler )
2021-05-08 14:25:55 +02:00
srv . SetClientInfoHandler ( server . ClientFormHandler )
2025-03-03 16:03:36 +01:00
return & s { srv }
2021-04-01 20:46:45 +02:00
}
2025-03-03 16:03:36 +01:00
// HandleTokenRequest wraps the oauth2 library's HandleTokenRequest function,
// providing some custom error handling (with more informative messages),
// and a slightly different token serialization format.
2022-06-11 10:39:39 +02:00
func ( s * s ) HandleTokenRequest ( r * http . Request ) ( map [ string ] interface { } , gtserror . WithCode ) {
ctx := r . Context ( )
gt , tgr , err := s . server . ValidationTokenRequest ( r )
if err != nil {
help := fmt . Sprintf ( "could not validate token request: %s" , err )
2023-02-28 11:38:34 +01:00
adv := HelpfulAdvice
if errors . Is ( err , oautherr . ErrUnsupportedGrantType ) {
adv = HelpfulAdviceGrant
}
return nil , gtserror . NewErrorBadRequest ( err , help , adv )
2022-06-11 10:39:39 +02:00
}
2025-03-03 16:03:36 +01:00
// Get access token + do our own nicer error handling.
2022-06-11 10:39:39 +02:00
ti , err := s . server . GetAccessToken ( ctx , gt , tgr )
2025-03-03 16:03:36 +01:00
switch {
case err == nil :
// No problem.
break
case errors . Is ( err , oautherr . ErrInvalidScope ) :
help := fmt . Sprintf ( "requested scope %s was not covered by client scope" , tgr . Scope )
return nil , gtserror . NewErrorForbidden ( err , help , HelpfulAdvice )
case errors . Is ( err , oautherr . ErrInvalidRedirectURI ) :
help := fmt . Sprintf ( "requested redirect URI %s was not covered by client redirect URIs" , tgr . RedirectURI )
return nil , gtserror . NewErrorForbidden ( err , help , HelpfulAdvice )
default :
help := fmt . Sprintf ( "could not get access token: %v" , err )
2022-10-08 13:49:56 +02:00
return nil , gtserror . NewErrorBadRequest ( err , help , HelpfulAdvice )
2022-06-11 10:39:39 +02:00
}
2025-03-03 16:03:36 +01:00
// Wrangle data a bit.
2022-06-11 10:39:39 +02:00
data := s . server . GetTokenData ( ti )
2022-07-28 16:43:42 +02:00
2025-03-03 16:03:36 +01:00
// Add created_at for Mastodon API compatibility.
data [ "created_at" ] = ti . GetAccessCreateAt ( ) . Unix ( )
// If expires_in is 0 or less, omit it
// from serialization so that clients don't
// interpret the token as already expired.
2022-07-28 16:43:42 +02:00
if expiresInI , ok := data [ "expires_in" ] ; ok {
2025-03-03 16:03:36 +01:00
// This will panic if expiresIn is
// not an int64, which is what we want.
if expiresInI . ( int64 ) <= 0 {
delete ( data , "expires_in" )
2022-07-28 16:43:42 +02:00
}
}
2022-06-11 10:39:39 +02:00
return data , nil
2021-04-01 20:46:45 +02:00
}
2022-10-08 13:49:56 +02:00
func ( s * s ) errorOrRedirect ( err error , w http . ResponseWriter , req * server . AuthorizeRequest ) gtserror . WithCode {
if req == nil {
return gtserror . NewErrorUnauthorized ( err , HelpfulAdvice )
}
data , _ , _ := s . server . GetErrorData ( err )
uri , err := s . server . GetRedirectURI ( req , data )
if err != nil {
return gtserror . NewErrorInternalError ( err , HelpfulAdvice )
}
w . Header ( ) . Set ( "Location" , uri )
w . WriteHeader ( http . StatusFound )
return nil
}
2021-04-01 20:46:45 +02:00
// HandleAuthorizeRequest wraps the oauth2 library's HandleAuthorizeRequest function
2022-10-08 13:49:56 +02:00
func ( s * s ) HandleAuthorizeRequest ( w http . ResponseWriter , r * http . Request ) gtserror . WithCode {
ctx := r . Context ( )
req , err := s . server . ValidationAuthorizeRequest ( r )
if err != nil {
return s . errorOrRedirect ( err , w , req )
}
// user authorization
userID , err := s . server . UserAuthorizationHandler ( w , r )
if err != nil {
return s . errorOrRedirect ( err , w , req )
}
if userID == "" {
help := "userID was empty"
return gtserror . NewErrorUnauthorized ( err , help , HelpfulAdvice )
}
req . UserID = userID
2025-03-03 16:03:36 +01:00
// Specify the scope of authorization.
2022-10-08 13:49:56 +02:00
if fn := s . server . AuthorizeScopeHandler ; fn != nil {
scope , err := fn ( w , r )
if err != nil {
return s . errorOrRedirect ( err , w , req )
} else if scope != "" {
req . Scope = scope
}
}
2025-03-03 16:03:36 +01:00
// Specify the expiration time of access token.
2022-10-08 13:49:56 +02:00
if fn := s . server . AccessTokenExpHandler ; fn != nil {
exp , err := fn ( w , r )
if err != nil {
return s . errorOrRedirect ( err , w , req )
}
req . AccessTokenExp = exp
}
ti , err := s . server . GetAuthorizeToken ( ctx , req )
if err != nil {
return s . errorOrRedirect ( err , w , req )
}
2025-03-03 16:03:36 +01:00
// If the redirect URI is empty, use the
// first of the client's redirect URIs.
2022-10-08 13:49:56 +02:00
if req . RedirectURI == "" {
client , err := s . server . Manager . GetClient ( ctx , req . ClientID )
2025-03-03 16:03:36 +01:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
// Real error.
err := gtserror . Newf ( "db error getting application with client id %s: %w" , req . ClientID , err )
return gtserror . NewErrorInternalError ( err )
}
if util . IsNil ( client ) {
// Application just not found.
2022-10-08 13:49:56 +02:00
return gtserror . NewErrorUnauthorized ( err , HelpfulAdvice )
}
2025-03-03 16:03:36 +01:00
// This will panic if client is not a
// *gtsmodel.Application, which is what we want.
req . RedirectURI = client . ( * gtsmodel . Application ) . RedirectURIs [ 0 ]
2022-10-08 13:49:56 +02:00
}
uri , err := s . server . GetRedirectURI ( req , s . server . GetAuthorizeData ( req . ResponseType , ti ) )
if err != nil {
return gtserror . NewErrorUnauthorized ( err , HelpfulAdvice )
}
if strings . Contains ( uri , OOBURI ) {
w . Header ( ) . Set ( "Location" , strings . ReplaceAll ( uri , OOBURI , OOBTokenPath ) )
} else {
w . Header ( ) . Set ( "Location" , uri )
}
w . WriteHeader ( http . StatusFound )
return nil
2021-04-01 20:46:45 +02:00
}
// ValidationBearerToken wraps the oauth2 library's ValidationBearerToken function
func ( s * s ) ValidationBearerToken ( r * http . Request ) ( oauth2 . TokenInfo , error ) {
return s . server . ValidationBearerToken ( r )
}
// GenerateUserAccessToken shortcuts the normal oauth flow to create an user-level
// bearer token *without* requiring that user to log in. This is useful when we
// need to create a token for new users who haven't validated their email or logged in yet.
//
// The ti parameter refers to an existing Application token that was used to make the upstream
// request. This token needs to be validated and exist in database in order to create a new token.
2021-10-04 15:24:19 +02:00
func ( s * s ) GenerateUserAccessToken ( ctx context . Context , ti oauth2 . TokenInfo , clientSecret string , userID string ) ( oauth2 . TokenInfo , error ) {
authToken , err := s . server . Manager . GenerateAuthToken ( ctx , oauth2 . Code , & oauth2 . TokenGenerateRequest {
2021-04-01 20:46:45 +02:00
ClientID : ti . GetClientID ( ) ,
ClientSecret : clientSecret ,
UserID : userID ,
RedirectURI : ti . GetRedirectURI ( ) ,
Scope : ti . GetScope ( ) ,
} )
if err != nil {
return nil , fmt . Errorf ( "error generating auth token: %s" , err )
}
if authToken == nil {
return nil , errors . New ( "generated auth token was empty" )
}
2023-02-17 12:02:29 +01:00
log . Tracef ( ctx , "obtained auth token: %+v" , authToken )
2021-04-01 20:46:45 +02:00
2021-10-04 15:24:19 +02:00
accessToken , err := s . server . Manager . GenerateAccessToken ( ctx , oauth2 . AuthorizationCode , & oauth2 . TokenGenerateRequest {
2021-04-01 20:46:45 +02:00
ClientID : authToken . GetClientID ( ) ,
ClientSecret : clientSecret ,
RedirectURI : authToken . GetRedirectURI ( ) ,
Scope : authToken . GetScope ( ) ,
Code : authToken . GetCode ( ) ,
} )
if err != nil {
return nil , fmt . Errorf ( "error generating user-level access token: %s" , err )
}
if accessToken == nil {
return nil , errors . New ( "generated user-level access token was empty" )
}
2023-02-17 12:02:29 +01:00
log . Tracef ( ctx , "obtained user-level access token: %+v" , accessToken )
2021-04-01 20:46:45 +02:00
return accessToken , nil
}
2021-06-19 11:18:55 +02:00
func ( s * s ) LoadAccessToken ( ctx context . Context , access string ) ( accessToken oauth2 . TokenInfo , err error ) {
return s . server . Manager . LoadAccessToken ( ctx , access )
}