2021-04-01 18:46:45 +00:00
/ *
GoToSocial
2021-12-20 17:42:19 +00:00
Copyright ( C ) 2021 - 2022 GoToSocial Authors admin @ gotosocial . org
2021-04-01 18:46:45 +00:00
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 (
"errors"
"fmt"
"net/http"
"net/url"
2021-12-11 16:50:00 +00:00
2021-04-01 18:46:45 +00:00
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
2021-07-23 08:36:28 +00:00
"github.com/google/uuid"
2021-12-11 16:50:00 +00:00
"github.com/superseriousbusiness/gotosocial/internal/api"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/api/model"
2022-08-08 08:40:51 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-05-21 13:48:26 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-06-08 18:38:03 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-04-01 18:46:45 +00:00
)
2022-06-08 18:38:03 +00:00
// helpfulAdvice is a handy hint to users;
// particularly important during the login flow
var 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"
2021-04-20 16:14:23 +00:00
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
2021-04-01 18:46:45 +00:00
// The idea here is to present an oauth authorize page to the user, with a button
2021-10-04 13:24:19 +00:00
// that they have to click to accept.
2021-04-20 16:14:23 +00:00
func ( m * Module ) AuthorizeGETHandler ( c * gin . Context ) {
2021-04-01 18:46:45 +00:00
s := sessions . Default ( c )
2021-12-11 16:50:00 +00:00
if _ , err := api . NegotiateAccept ( c , api . HTMLAcceptHeaders ... ) ; err != nil {
2022-06-08 18:38:03 +00:00
api . ErrorHandler ( c , gtserror . NewErrorNotAcceptable ( err , err . Error ( ) ) , m . processor . InstanceGet )
2021-12-11 16:50:00 +00:00
return
}
2021-04-01 18:46:45 +00:00
// UserID will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
2021-07-08 09:32:31 +00:00
userID , ok := s . Get ( sessionUserID ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || userID == "" {
2021-07-08 09:32:31 +00:00
form := & model . OAuthAuthorize { }
2022-06-08 18:38:03 +00:00
if err := c . ShouldBind ( form ) ; err != nil {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , helpfulAdvice ) , m . processor . InstanceGet )
2021-07-08 09:32:31 +00:00
return
}
2022-06-08 18:38:03 +00:00
if errWithCode := saveAuthFormToSession ( s , form ) ; errWithCode != nil {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-07-08 09:32:31 +00:00
return
2021-04-01 18:46:45 +00:00
}
2022-06-08 18:38:03 +00:00
2021-07-23 08:36:28 +00:00
c . Redirect ( http . StatusSeeOther , AuthSignInPath )
2021-04-01 18:46:45 +00:00
return
}
2022-06-08 18:38:03 +00:00
// use session information to validate app, user, and account for this request
2021-07-08 09:32:31 +00:00
clientID , ok := s . Get ( sessionClientID ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || clientID == "" {
2022-06-08 18:38:03 +00:00
m . clearSession ( s )
err := fmt . Errorf ( "key %s was not found in session" , sessionClientID )
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , helpfulAdvice ) , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
return
}
2022-06-08 18:38:03 +00:00
2021-08-25 13:34:33 +00:00
app := & gtsmodel . Application { }
2021-08-26 17:56:40 +00:00
if err := m . db . GetWhere ( c . Request . Context ( ) , [ ] db . Where { { Key : sessionClientID , Value : clientID } } , app ) ; err != nil {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
safe := fmt . Sprintf ( "application for %s %s could not be retrieved" , sessionClientID , clientID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
errWithCode = gtserror . NewErrorBadRequest ( err , safe , helpfulAdvice )
} else {
errWithCode = gtserror . NewErrorInternalError ( err , safe , helpfulAdvice )
}
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
return
}
2021-08-25 13:34:33 +00:00
user := & gtsmodel . User { }
2021-08-26 17:56:40 +00:00
if err := m . db . GetByID ( c . Request . Context ( ) , userID , user ) ; err != nil {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
safe := fmt . Sprintf ( "user with id %s could not be retrieved" , userID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
errWithCode = gtserror . NewErrorBadRequest ( err , safe , helpfulAdvice )
} else {
errWithCode = gtserror . NewErrorInternalError ( err , safe , helpfulAdvice )
}
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
return
}
2022-06-08 18:38:03 +00:00
2021-08-25 13:34:33 +00:00
acct , err := m . db . GetAccountByID ( c . Request . Context ( ) , user . AccountID )
if err != nil {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
safe := fmt . Sprintf ( "account with id %s could not be retrieved" , user . AccountID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
errWithCode = gtserror . NewErrorBadRequest ( err , safe , helpfulAdvice )
} else {
errWithCode = gtserror . NewErrorInternalError ( err , safe , helpfulAdvice )
}
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-02-07 11:04:31 +00:00
return
}
2022-06-08 18:38:03 +00:00
if ensureUserIsAuthorizedOrRedirect ( c , user , acct ) {
2021-04-01 18:46:45 +00:00
return
}
// Finally we should also get the redirect and scope of this particular request, as stored in the session.
2021-07-08 09:32:31 +00:00
redirect , ok := s . Get ( sessionRedirectURI ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || redirect == "" {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "key %s was not found in session" , sessionRedirectURI )
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , helpfulAdvice ) , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
return
}
2022-06-08 18:38:03 +00:00
2021-07-08 09:32:31 +00:00
scope , ok := s . Get ( sessionScope ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || scope == "" {
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "key %s was not found in session" , sessionScope )
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , helpfulAdvice ) , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
return
}
2022-08-08 08:40:51 +00:00
instance , errWithCode := m . processor . InstanceGet ( c . Request . Context ( ) , config . GetHost ( ) )
if errWithCode != nil {
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
return
}
2021-04-01 18:46:45 +00:00
// the authorize template will display a form to the user where they can get some information
// about the app that's trying to authorize, and the scope of the request.
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
c . HTML ( http . StatusOK , "authorize.tmpl" , gin . H {
"appname" : app . Name ,
"appwebsite" : app . Website ,
"redirect" : redirect ,
2022-06-08 18:38:03 +00:00
"scope" : scope ,
2021-04-01 18:46:45 +00:00
"user" : acct . Username ,
2022-08-08 08:40:51 +00:00
"instance" : instance ,
2021-04-01 18:46:45 +00:00
} )
}
2021-04-20 16:14:23 +00:00
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
2021-04-01 18:46:45 +00:00
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
// so we should proceed with the authentication flow and generate an oauth token for them if we can.
2021-04-20 16:14:23 +00:00
func ( m * Module ) AuthorizePOSTHandler ( c * gin . Context ) {
2021-04-01 18:46:45 +00:00
s := sessions . Default ( c )
// We need to retrieve the original form submitted to the authorizeGEThandler, and
// recreate it on the request so that it can be used further by the oauth2 library.
2021-07-23 08:36:28 +00:00
errs := [ ] string { }
2021-07-08 09:32:31 +00:00
forceLogin , ok := s . Get ( sessionForceLogin ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok {
2021-07-23 08:36:28 +00:00
forceLogin = "false"
2021-04-01 18:46:45 +00:00
}
2021-07-08 09:32:31 +00:00
responseType , ok := s . Get ( sessionResponseType ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || responseType == "" {
2022-06-08 18:38:03 +00:00
errs = append ( errs , fmt . Sprintf ( "key %s was not found in session" , sessionResponseType ) )
2021-04-01 18:46:45 +00:00
}
2021-07-08 09:32:31 +00:00
clientID , ok := s . Get ( sessionClientID ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || clientID == "" {
2022-06-08 18:38:03 +00:00
errs = append ( errs , fmt . Sprintf ( "key %s was not found in session" , sessionClientID ) )
2021-04-01 18:46:45 +00:00
}
2021-07-08 09:32:31 +00:00
redirectURI , ok := s . Get ( sessionRedirectURI ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok || redirectURI == "" {
2022-06-08 18:38:03 +00:00
errs = append ( errs , fmt . Sprintf ( "key %s was not found in session" , sessionRedirectURI ) )
2021-04-01 18:46:45 +00:00
}
2021-07-08 09:32:31 +00:00
scope , ok := s . Get ( sessionScope ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok {
2022-06-08 18:38:03 +00:00
errs = append ( errs , fmt . Sprintf ( "key %s was not found in session" , sessionScope ) )
2021-04-01 18:46:45 +00:00
}
2021-07-08 09:32:31 +00:00
2022-07-28 14:43:27 +00:00
var clientState string
if s , ok := s . Get ( sessionClientState ) . ( string ) ; ok {
clientState = s
}
2021-07-08 09:32:31 +00:00
userID , ok := s . Get ( sessionUserID ) . ( string )
2021-04-01 18:46:45 +00:00
if ! ok {
2022-06-08 18:38:03 +00:00
errs = append ( errs , fmt . Sprintf ( "key %s was not found in session" , sessionUserID ) )
}
if len ( errs ) != 0 {
errs = append ( errs , helpfulAdvice )
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( errors . New ( "one or more missing keys on session during AuthorizePOSTHandler" ) , errs ... ) , m . processor . InstanceGet )
return
2021-04-01 18:46:45 +00:00
}
2021-07-07 13:46:42 +00:00
2022-02-07 11:04:31 +00:00
user := & gtsmodel . User { }
if err := m . db . GetByID ( c . Request . Context ( ) , userID , user ) ; err != nil {
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
safe := fmt . Sprintf ( "user with id %s could not be retrieved" , userID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
errWithCode = gtserror . NewErrorBadRequest ( err , safe , helpfulAdvice )
} else {
errWithCode = gtserror . NewErrorInternalError ( err , safe , helpfulAdvice )
}
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-02-07 11:04:31 +00:00
return
}
2022-06-08 18:38:03 +00:00
2022-02-07 11:04:31 +00:00
acct , err := m . db . GetAccountByID ( c . Request . Context ( ) , user . AccountID )
if err != nil {
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
safe := fmt . Sprintf ( "account with id %s could not be retrieved" , user . AccountID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
errWithCode = gtserror . NewErrorBadRequest ( err , safe , helpfulAdvice )
} else {
errWithCode = gtserror . NewErrorInternalError ( err , safe , helpfulAdvice )
}
api . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-02-07 11:04:31 +00:00
return
}
2022-06-08 18:38:03 +00:00
if ensureUserIsAuthorizedOrRedirect ( c , user , acct ) {
2022-02-07 11:04:31 +00:00
return
}
2022-06-08 18:38:03 +00:00
// we're done with the session now, so just clear it out
2021-07-23 08:36:28 +00:00
m . clearSession ( s )
2022-06-08 18:38:03 +00:00
// we have to set the values on the request form
// so that they're picked up by the oauth server
c . Request . Form = url . Values {
sessionForceLogin : { forceLogin } ,
sessionResponseType : { responseType } ,
sessionClientID : { clientID } ,
sessionRedirectURI : { redirectURI } ,
sessionScope : { scope } ,
sessionUserID : { userID } ,
2021-07-07 13:46:42 +00:00
}
2021-04-01 18:46:45 +00:00
2022-07-28 14:43:27 +00:00
if clientState != "" {
c . Request . Form . Set ( "state" , clientState )
}
2022-06-11 08:39:39 +00:00
if err := m . processor . OAuthHandleAuthorizeRequest ( c . Writer , c . Request ) ; err != nil {
2022-06-08 18:38:03 +00:00
api . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) , helpfulAdvice ) , m . processor . InstanceGet )
2021-04-01 18:46:45 +00:00
}
}
2022-06-08 18:38:03 +00:00
// saveAuthFormToSession checks the given OAuthAuthorize form,
// and stores the values in the form into the session.
func saveAuthFormToSession ( s sessions . Session , form * model . OAuthAuthorize ) gtserror . WithCode {
if form == nil {
err := errors . New ( "OAuthAuthorize form was nil" )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) , helpfulAdvice )
}
if form . ResponseType == "" {
err := errors . New ( "field response_type was not set on OAuthAuthorize form" )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) , helpfulAdvice )
}
if form . ClientID == "" {
err := errors . New ( "field client_id was not set on OAuthAuthorize form" )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) , helpfulAdvice )
}
if form . RedirectURI == "" {
err := errors . New ( "field redirect_uri was not set on OAuthAuthorize form" )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) , helpfulAdvice )
2021-04-01 18:46:45 +00:00
}
// set default scope to read
if form . Scope == "" {
form . Scope = "read"
}
// save these values from the form so we can use them elsewhere in the session
2021-07-08 09:32:31 +00:00
s . Set ( sessionForceLogin , form . ForceLogin )
s . Set ( sessionResponseType , form . ResponseType )
s . Set ( sessionClientID , form . ClientID )
s . Set ( sessionRedirectURI , form . RedirectURI )
s . Set ( sessionScope , form . Scope )
2022-07-28 14:43:27 +00:00
s . Set ( sessionInternalState , uuid . NewString ( ) )
s . Set ( sessionClientState , form . State )
2022-06-08 18:38:03 +00:00
if err := s . Save ( ) ; err != nil {
err := fmt . Errorf ( "error saving form values onto session: %s" , err )
return gtserror . NewErrorInternalError ( err , helpfulAdvice )
}
return nil
2021-04-01 18:46:45 +00:00
}
2022-02-07 11:04:31 +00:00
2022-06-08 18:38:03 +00:00
func ensureUserIsAuthorizedOrRedirect ( ctx * gin . Context , user * gtsmodel . User , account * gtsmodel . Account ) ( redirected bool ) {
2022-02-07 11:04:31 +00:00
if user . ConfirmedAt . IsZero ( ) {
ctx . Redirect ( http . StatusSeeOther , CheckYourEmailPath )
2022-06-08 18:38:03 +00:00
redirected = true
return
2022-02-07 11:04:31 +00:00
}
if ! user . Approved {
ctx . Redirect ( http . StatusSeeOther , WaitForApprovalPath )
2022-06-08 18:38:03 +00:00
redirected = true
return
2022-02-07 11:04:31 +00:00
}
2022-06-08 18:38:03 +00:00
if user . Disabled || ! account . SuspendedAt . IsZero ( ) {
2022-02-07 11:04:31 +00:00
ctx . Redirect ( http . StatusSeeOther , AccountDisabledPath )
2022-06-08 18:38:03 +00:00
redirected = true
return
2022-02-07 11:04:31 +00:00
}
2022-06-08 18:38:03 +00:00
return
2022-02-07 11:04:31 +00:00
}