2023-03-12 15:00:57 +00: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 18:46:45 +00:00
package oauth
import (
"context"
2023-02-28 10:38:34 +00:00
"errors"
2021-04-01 18:46:45 +00:00
"fmt"
"net/http"
2022-10-08 11:49:56 +00:00
"strings"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-06-11 08:39:39 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/oauth2/v4"
2023-02-28 10:38:34 +00:00
oautherr "github.com/superseriousbusiness/oauth2/v4/errors"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/oauth2/v4/manage"
"github.com/superseriousbusiness/oauth2/v4/server"
)
const (
2021-04-19 17:42:19 +00: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 18:46:45 +00: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 16:14:23 +00:00
// SessionAuthorizedApplication is the key set in the gin context for the Application
2021-04-01 18:46:45 +00: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 11:49:56 +00: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 15:47:42 +00:00
OOBTokenPath = "/oauth/oob" // #nosec G101 else we get a hardcoded credentials warning
2022-10-08 11:49:56 +00:00
// HelpfulAdvice is a handy hint to users;
// particularly important during the login flow
2023-02-28 10:38:34 +00:00
HelpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
HelpfulAdviceGrant = "If you arrived at this error during a login/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 18:46:45 +00:00
)
// Server wraps some oauth2 server functions in an interface, exposing only what is needed
type Server interface {
2022-06-11 08:39:39 +00:00
HandleTokenRequest ( r * http . Request ) ( map [ string ] interface { } , gtserror . WithCode )
2022-10-08 11:49:56 +00:00
HandleAuthorizeRequest ( w http . ResponseWriter , r * http . Request ) gtserror . WithCode
2021-04-01 18:46:45 +00:00
ValidationBearerToken ( r * http . Request ) ( oauth2 . TokenInfo , error )
2021-10-04 13:24:19 +00:00
GenerateUserAccessToken ( ctx context . Context , ti oauth2 . TokenInfo , clientSecret string , userID string ) ( accessToken oauth2 . TokenInfo , err error )
2021-06-19 09:18:55 +00:00
LoadAccessToken ( ctx context . Context , access string ) ( accessToken oauth2 . TokenInfo , err error )
2021-04-01 18:46:45 +00:00
}
// s fulfils the Server interface using the underlying oauth2 server
type s struct {
server * server . Server
}
2021-05-08 12:25:55 +00:00
// New returns a new oauth server that implements the Server interface
2021-10-11 12:37:33 +00:00
func New ( ctx context . Context , database db . Basic ) Server {
ts := newTokenStore ( ctx , database )
2021-05-08 12:25:55 +00:00
cs := NewClientStore ( database )
2021-04-01 18:46:45 +00:00
2021-05-08 12:25:55 +00:00
manager := manage . NewDefaultManager ( )
manager . MapTokenStorage ( ts )
manager . MapClientStorage ( cs )
2021-05-10 14:29:05 +00:00
manager . SetAuthorizeCodeTokenCfg ( & manage . Config {
AccessTokenExp : 0 , // access tokens don't expire -- they must be revoked
IsGenerateRefresh : false , // don't use refresh tokens
} )
2021-05-08 12:25:55 +00:00
sc := & 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 ,
} ,
2023-09-28 09:21:19 +00:00
AllowedCodeChallengeMethods : [ ] oauth2 . CodeChallengeMethod {
oauth2 . CodeChallengePlain ,
oauth2 . CodeChallengeS256 ,
} ,
2021-04-01 18:46:45 +00:00
}
2021-05-08 12:25:55 +00:00
srv := server . NewServer ( sc , manager )
2023-02-28 10:38:34 +00:00
srv . SetInternalErrorHandler ( func ( err error ) * oautherr . Response {
2023-02-17 11:02:29 +00:00
log . Errorf ( nil , "internal oauth error: %s" , err )
2021-05-08 12:25:55 +00:00
return nil
} )
2021-04-01 18:46:45 +00:00
2023-02-28 10:38:34 +00:00
srv . SetResponseErrorHandler ( func ( re * oautherr . Response ) {
2023-02-17 11:02:29 +00:00
log . Errorf ( nil , "internal response error: %s" , re . Error )
2021-05-08 12:25:55 +00:00
} )
2021-04-01 18:46:45 +00:00
2021-05-08 12:25:55 +00:00
srv . SetUserAuthorizationHandler ( func ( w http . ResponseWriter , r * http . Request ) ( string , error ) {
userID := r . FormValue ( "userid" )
if userID == "" {
return "" , errors . New ( "userid was empty" )
2021-04-01 18:46:45 +00:00
}
2021-05-08 12:25:55 +00:00
return userID , nil
} )
srv . SetClientInfoHandler ( server . ClientFormHandler )
return & s {
server : srv ,
2021-04-01 18:46:45 +00:00
}
}
// HandleTokenRequest wraps the oauth2 library's HandleTokenRequest function
2022-06-11 08:39:39 +00: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 10:38:34 +00:00
adv := HelpfulAdvice
if errors . Is ( err , oautherr . ErrUnsupportedGrantType ) {
adv = HelpfulAdviceGrant
}
return nil , gtserror . NewErrorBadRequest ( err , help , adv )
2022-06-11 08:39:39 +00:00
}
ti , err := s . server . GetAccessToken ( ctx , gt , tgr )
if err != nil {
help := fmt . Sprintf ( "could not get access token: %s" , err )
2022-10-08 11:49:56 +00:00
return nil , gtserror . NewErrorBadRequest ( err , help , HelpfulAdvice )
2022-06-11 08:39:39 +00:00
}
data := s . server . GetTokenData ( ti )
2022-07-28 14:43:42 +00:00
if expiresInI , ok := data [ "expires_in" ] ; ok {
switch expiresIn := expiresInI . ( type ) {
case int64 :
// remove this key from the returned map
// if the value is 0 or less, so that clients
// don't interpret the token as already expired
if expiresIn <= 0 {
delete ( data , "expires_in" )
}
default :
err := errors . New ( "expires_in was set on token response, but was not an int64" )
2022-10-08 11:49:56 +00:00
return nil , gtserror . NewErrorInternalError ( err , HelpfulAdvice )
2022-07-28 14:43:42 +00:00
}
}
// add this for mastodon api compatibility
2022-06-11 08:39:39 +00:00
data [ "created_at" ] = ti . GetAccessCreateAt ( ) . Unix ( )
return data , nil
2021-04-01 18:46:45 +00:00
}
2022-10-08 11:49:56 +00: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 18:46:45 +00:00
// HandleAuthorizeRequest wraps the oauth2 library's HandleAuthorizeRequest function
2022-10-08 11:49:56 +00: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
// specify the scope of authorization
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
}
}
// specify the expiration time of access token
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 )
}
// If the redirect URI is empty, the default domain provided by the client is used.
if req . RedirectURI == "" {
client , err := s . server . Manager . GetClient ( ctx , req . ClientID )
if err != nil {
return gtserror . NewErrorUnauthorized ( err , HelpfulAdvice )
}
req . RedirectURI = client . GetDomain ( )
}
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 18:46:45 +00: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 13:24:19 +00: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 18:46:45 +00: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 11:02:29 +00:00
log . Tracef ( ctx , "obtained auth token: %+v" , authToken )
2021-04-01 18:46:45 +00:00
2021-10-04 13:24:19 +00:00
accessToken , err := s . server . Manager . GenerateAccessToken ( ctx , oauth2 . AuthorizationCode , & oauth2 . TokenGenerateRequest {
2021-04-01 18:46:45 +00: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 11:02:29 +00:00
log . Tracef ( ctx , "obtained user-level access token: %+v" , accessToken )
2021-04-01 18:46:45 +00:00
return accessToken , nil
}
2021-06-19 09:18:55 +00:00
func ( s * s ) LoadAccessToken ( ctx context . Context , access string ) ( accessToken oauth2 . TokenInfo , err error ) {
return s . server . Manager . LoadAccessToken ( ctx , access )
}