2021-07-23 09:36:28 +01:00
/ *
GoToSocial
2021-12-20 17:42:19 +00:00
Copyright ( C ) 2021 - 2022 GoToSocial Authors admin @ gotosocial . org
2021-07-23 09:36:28 +01: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 (
2021-08-25 14:34:33 +01:00
"context"
2021-07-23 09:36:28 +01:00
"errors"
"fmt"
"net"
"net/http"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
2023-01-02 12:10:50 +00:00
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
2022-12-06 13:15:56 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-07-23 09:36:28 +01:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-06-08 19:38:03 +01:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-07-23 09:36:28 +01:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-10-08 12:49:56 +01:00
"github.com/superseriousbusiness/gotosocial/internal/oauth"
2021-07-23 09:36:28 +01:00
"github.com/superseriousbusiness/gotosocial/internal/oidc"
2021-09-01 17:29:25 +01:00
"github.com/superseriousbusiness/gotosocial/internal/validate"
2021-07-23 09:36:28 +01:00
)
2022-12-06 13:15:56 +00:00
// extraInfo wraps a form-submitted username and transmitted name
type extraInfo struct {
Username string ` form:"username" `
Name string ` form:"name" ` // note that this is only used for re-rendering the page in case of an error
}
2021-07-23 09:36:28 +01:00
// CallbackGETHandler parses a token from an external auth provider.
func ( m * Module ) CallbackGETHandler ( c * gin . Context ) {
2023-01-02 12:10:50 +00:00
if ! config . GetOIDCEnabled ( ) {
err := errors . New ( "oidc is not enabled for this server" )
apiutil . ErrorHandler ( c , gtserror . NewErrorNotFound ( err , err . Error ( ) ) , m . processor . InstanceGet )
return
}
2021-07-23 09:36:28 +01:00
s := sessions . Default ( c )
2022-06-08 19:38:03 +01:00
// check the query vs session state parameter to mitigate csrf
// https://auth0.com/docs/secure/attack-protection/state-parameters
2022-07-28 15:43:27 +01:00
returnedInternalState := c . Query ( callbackStateParam )
if returnedInternalState == "" {
2021-07-23 09:36:28 +01:00
m . clearSession ( s )
2022-06-08 19:38:03 +01:00
err := fmt . Errorf ( "%s parameter not found on callback query" , callbackStateParam )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-07-28 15:43:27 +01:00
savedInternalStateI := s . Get ( sessionInternalState )
savedInternalState , ok := savedInternalStateI . ( string )
2021-07-23 09:36:28 +01:00
if ! ok {
m . clearSession ( s )
2022-07-28 15:43:27 +01:00
err := fmt . Errorf ( "key %s was not found in session" , sessionInternalState )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-07-28 15:43:27 +01:00
if returnedInternalState != savedInternalState {
2021-07-23 09:36:28 +01:00
m . clearSession ( s )
2022-07-28 15:43:27 +01:00
err := errors . New ( "mismatch between callback state and saved state" )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorUnauthorized ( err , err . Error ( ) ) , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-06-08 19:38:03 +01:00
// retrieve stored claims using code
2021-07-23 09:36:28 +01:00
code := c . Query ( callbackCodeParam )
2022-06-08 19:38:03 +01:00
if code == "" {
m . clearSession ( s )
err := fmt . Errorf ( "%s parameter not found on callback query" , callbackCodeParam )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGet )
2022-06-08 19:38:03 +01:00
return
}
2021-07-23 09:36:28 +01:00
2022-06-08 19:38:03 +01:00
claims , errWithCode := m . idp . HandleCallback ( c . Request . Context ( ) , code )
if errWithCode != nil {
2021-07-23 09:36:28 +01:00
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-06-08 19:38:03 +01:00
// We can use the client_id on the session to retrieve
// info about the app associated with the client_id
2021-07-23 09:36:28 +01:00
clientID , ok := s . Get ( sessionClientID ) . ( string )
if ! ok || clientID == "" {
m . clearSession ( s )
2022-06-08 19:38:03 +01:00
err := fmt . Errorf ( "key %s was not found in session" , sessionClientID )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-06-08 19:38:03 +01:00
2021-08-26 18:56:40 +01:00
app := & gtsmodel . Application { }
if err := m . db . GetWhere ( c . Request . Context ( ) , [ ] db . Where { { Key : sessionClientID , Value : clientID } } , app ) ; err != nil {
2021-07-23 09:36:28 +01:00
m . clearSession ( s )
2022-06-08 19:38:03 +01:00
safe := fmt . Sprintf ( "application for %s %s could not be retrieved" , sessionClientID , clientID )
var errWithCode gtserror . WithCode
if err == db . ErrNoEntries {
2022-10-08 12:49:56 +01:00
errWithCode = gtserror . NewErrorBadRequest ( err , safe , oauth . HelpfulAdvice )
2022-06-08 19:38:03 +01:00
} else {
2022-10-08 12:49:56 +01:00
errWithCode = gtserror . NewErrorInternalError ( err , safe , oauth . HelpfulAdvice )
2022-06-08 19:38:03 +01:00
}
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-12-06 13:15:56 +00:00
user , errWithCode := m . fetchUserForClaims ( c . Request . Context ( ) , claims , net . IP ( c . ClientIP ( ) ) , app . ID )
2022-06-08 19:38:03 +01:00
if errWithCode != nil {
2021-07-23 09:36:28 +01:00
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2022-12-06 13:15:56 +00:00
if user == nil {
// no user exists yet - let's ask them for their preferred username
instance , errWithCode := m . processor . InstanceGet ( c . Request . Context ( ) , config . GetHost ( ) )
if errWithCode != nil {
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
2021-07-23 09:36:28 +01:00
2022-12-06 13:15:56 +00:00
// store the claims in the session - that way we know the user is authenticated when processing the form later
s . Set ( sessionClaims , claims )
s . Set ( sessionAppID , app . ID )
if err := s . Save ( ) ; err != nil {
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
c . HTML ( http . StatusOK , "finalize.tmpl" , gin . H {
"instance" : instance ,
"name" : claims . Name ,
"preferredUsername" : claims . PreferredUsername ,
} )
return
}
2021-07-23 09:36:28 +01:00
s . Set ( sessionUserID , user . ID )
if err := s . Save ( ) ; err != nil {
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGet )
2021-07-23 09:36:28 +01:00
return
}
2023-01-02 12:10:50 +00:00
c . Redirect ( http . StatusFound , "/oauth" + OauthAuthorizePath )
2022-12-06 13:15:56 +00:00
}
// FinalizePOSTHandler registers the user after additional data has been provided
func ( m * Module ) FinalizePOSTHandler ( c * gin . Context ) {
s := sessions . Default ( c )
2021-07-23 09:36:28 +01:00
2022-12-06 13:15:56 +00:00
form := & extraInfo { }
if err := c . ShouldBind ( form ) ; err != nil {
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
// since we have multiple possible validation error, `validationError` is a shorthand for rendering them
validationError := func ( err error ) {
instance , errWithCode := m . processor . InstanceGet ( c . Request . Context ( ) , config . GetHost ( ) )
if errWithCode != nil {
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
c . HTML ( http . StatusOK , "finalize.tmpl" , gin . H {
"instance" : instance ,
"name" : form . Name ,
"preferredUsername" : form . Username ,
"error" : err ,
} )
}
// check if the username conforms to the spec
if err := validate . Username ( form . Username ) ; err != nil {
validationError ( err )
return
}
// see if the username is still available
usernameAvailable , err := m . db . IsUsernameAvailable ( c . Request . Context ( ) , form . Username )
if err != nil {
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
if ! usernameAvailable {
validationError ( fmt . Errorf ( "Username %s is already taken" , form . Username ) )
return
}
// retrieve the information previously set by the oidc logic
appID , ok := s . Get ( sessionAppID ) . ( string )
if ! ok {
err := fmt . Errorf ( "key %s was not found in session" , sessionAppID )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
// retrieve the claims returned by the IDP. Having this present means that we previously already verified these claims
claims , ok := s . Get ( sessionClaims ) . ( * oidc . Claims )
if ! ok {
err := fmt . Errorf ( "key %s was not found in session" , sessionClaims )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
// we're now ready to actually create the user
user , errWithCode := m . createUserFromOIDC ( c . Request . Context ( ) , claims , form , net . IP ( c . ClientIP ( ) ) , appID )
if errWithCode != nil {
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
s . Delete ( sessionClaims )
s . Delete ( sessionAppID )
s . Set ( sessionUserID , user . ID )
if err := s . Save ( ) ; err != nil {
m . clearSession ( s )
2023-01-02 12:10:50 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGet )
2022-12-06 13:15:56 +00:00
return
}
2023-01-02 12:10:50 +00:00
c . Redirect ( http . StatusFound , "/oauth" + OauthAuthorizePath )
2021-07-23 09:36:28 +01:00
}
2022-12-06 13:15:56 +00:00
func ( m * Module ) fetchUserForClaims ( ctx context . Context , claims * oidc . Claims , ip net . IP , appID string ) ( * gtsmodel . User , gtserror . WithCode ) {
if claims . Sub == "" {
err := errors . New ( "no sub claim found - is your provider OIDC compliant?" )
2022-06-08 19:38:03 +01:00
return nil , gtserror . NewErrorBadRequest ( err , err . Error ( ) )
2021-07-23 09:36:28 +01:00
}
2022-12-06 13:15:56 +00:00
user , err := m . db . GetUserByExternalID ( ctx , claims . Sub )
2021-07-23 09:36:28 +01:00
if err == nil {
return user , nil
}
2021-08-20 11:26:56 +01:00
if err != db . ErrNoEntries {
2022-12-06 13:15:56 +00:00
err := fmt . Errorf ( "error checking database for externalID %s: %s" , claims . Sub , err )
2022-06-08 19:38:03 +01:00
return nil , gtserror . NewErrorInternalError ( err )
2021-07-23 09:36:28 +01:00
}
2022-12-06 13:15:56 +00:00
if ! config . GetOIDCLinkExisting ( ) {
return nil , nil
2021-07-23 09:36:28 +01:00
}
2022-12-06 13:15:56 +00:00
// fallback to email if we want to link existing users
user , err = m . db . GetUserByEmailAddress ( ctx , claims . Email )
if err == db . ErrNoEntries {
return nil , nil
} else if err != nil {
2022-06-08 19:38:03 +01:00
err := fmt . Errorf ( "error checking database for email %s: %s" , claims . Email , err )
return nil , gtserror . NewErrorInternalError ( err )
2021-07-23 09:36:28 +01:00
}
2022-12-06 13:15:56 +00:00
// at this point we have found a matching user but still need to link the newly received external ID
2021-07-23 09:36:28 +01:00
2022-12-06 13:15:56 +00:00
user . ExternalID = claims . Sub
err = m . db . UpdateUser ( ctx , user , "external_id" )
if err != nil {
err := fmt . Errorf ( "error linking existing user %s: %s" , claims . Email , err )
return nil , gtserror . NewErrorInternalError ( err )
}
return user , nil
}
2021-07-23 09:36:28 +01:00
2022-12-06 13:15:56 +00:00
func ( m * Module ) createUserFromOIDC ( ctx context . Context , claims * oidc . Claims , extraInfo * extraInfo , ip net . IP , appID string ) ( * gtsmodel . User , gtserror . WithCode ) {
2021-07-23 09:36:28 +01:00
// check if the email address is available for use; if it's not there's nothing we can so
2021-08-25 14:34:33 +01:00
emailAvailable , err := m . db . IsEmailAvailable ( ctx , claims . Email )
if err != nil {
2022-06-08 19:38:03 +01:00
return nil , gtserror . NewErrorBadRequest ( err )
2021-07-23 09:36:28 +01:00
}
2021-08-25 14:34:33 +01:00
if ! emailAvailable {
2022-12-06 13:15:56 +00:00
help := "The email address given to us by your authentication provider already exists in our records and the server administrator has not enabled account migration"
return nil , gtserror . NewErrorConflict ( fmt . Errorf ( "email address %s is not available" , claims . Email ) , help )
2021-07-23 09:36:28 +01:00
}
// check if the user is in any recognised admin groups
var admin bool
for _ , g := range claims . Groups {
if strings . EqualFold ( g , "admin" ) || strings . EqualFold ( g , "admins" ) {
admin = true
}
}
2022-01-31 15:03:47 +00:00
// We still need to set *a* password even if it's not a password the user will end up using, so set something random.
// We'll just set two uuids on top of each other, which should be long + random enough to baffle any attempts to crack.
2021-07-23 09:36:28 +01:00
//
2022-01-31 15:03:47 +00:00
// If the user ever wants to log in using gts password rather than oidc flow, they'll have to request a password reset, which is fine
2021-07-23 09:36:28 +01:00
password := uuid . NewString ( ) + uuid . NewString ( )
2022-01-31 15:03:47 +00:00
// Since this user is created via oidc, which has been set up by the admin, we can assume that the account is already
// implicitly approved, and that the email address has already been verified: otherwise, we end up in situations where
// the admin first approves the user in OIDC, and then has to approve them again in GoToSocial, which doesn't make sense.
//
// In other words, if a user logs in via OIDC, they should be able to use their account straight away.
//
// See: https://github.com/superseriousbusiness/gotosocial/issues/357
requireApproval := false
emailVerified := true
2021-07-23 09:36:28 +01:00
// create the user! this will also create an account and store it in the database so we don't need to do that here
2022-12-06 13:15:56 +00:00
user , err := m . db . NewSignup ( ctx , extraInfo . Username , "" , requireApproval , claims . Email , password , ip , "" , appID , emailVerified , claims . Sub , admin )
2021-07-23 09:36:28 +01:00
if err != nil {
2022-06-08 19:38:03 +01:00
return nil , gtserror . NewErrorInternalError ( err )
2021-07-23 09:36:28 +01:00
}
return user , nil
}