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-07-23 09:36:28 +01:00
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"
2024-02-27 15:07:29 +00:00
"slices"
2021-07-23 09:36:28 +01:00
"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" )
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorNotFound ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
2023-01-02 12:10:50 +00:00
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorUnauthorized ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGetV1 )
2021-07-23 09:36:28 +01:00
return
}
2022-06-08 19:38:03 +01:00
2023-08-11 13:58:47 +01:00
app , err := m . db . GetApplicationByClientID ( c . Request . Context ( ) , clientID )
2023-08-10 15:08:41 +01:00
if 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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
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
2023-02-02 13:08:13 +00:00
instance , errWithCode := m . processor . InstanceGetV1 ( c . Request . Context ( ) )
2022-12-06 13:15:56 +00:00
if errWithCode != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGetV1 )
2022-12-06 13:15:56 +00:00
return
}
2023-12-27 10:23:52 +00:00
page := apiutil . WebPage {
Template : "finalize.tmpl" ,
Instance : instance ,
Extra : map [ string ] any {
"name" : claims . Name ,
"preferredUsername" : claims . PreferredUsername ,
} ,
}
apiutil . TemplateWebPage ( c , page )
2022-12-06 13:15:56 +00:00
return
}
2024-02-27 15:07:29 +00:00
// Check user permissions on login
if ! allowedGroup ( claims . Groups ) {
err := fmt . Errorf ( "User groups %+v do not include an allowed group" , claims . Groups )
apiutil . ErrorHandler ( c , gtserror . NewErrorUnauthorized ( err , err . Error ( ) ) , m . processor . InstanceGetV1 )
return
}
2021-07-23 09:36:28 +01:00
s . Set ( sessionUserID , user . ID )
if err := s . Save ( ) ; err != nil {
m . clearSession ( s )
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGetV1 )
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 ) {
2023-02-02 13:08:13 +00:00
instance , errWithCode := m . processor . InstanceGetV1 ( c . Request . Context ( ) )
2022-12-06 13:15:56 +00:00
if errWithCode != nil {
2023-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
2022-12-06 13:15:56 +00:00
return
}
2023-12-27 10:23:52 +00:00
page := apiutil . WebPage {
Template : "finalize.tmpl" ,
Instance : instance ,
Extra : map [ string ] any {
"name" : form . Name ,
"preferredUsername" : form . Username ,
"error" : err ,
} ,
}
apiutil . TemplateWebPage ( c , page )
2022-12-06 13:15:56 +00:00
}
// 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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorBadRequest ( err , oauth . HelpfulAdvice ) , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , errWithCode , m . processor . InstanceGetV1 )
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-02-02 13:08:13 +00:00
apiutil . ErrorHandler ( c , gtserror . NewErrorInternalError ( err ) , m . processor . InstanceGetV1 )
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 ) {
2023-07-23 11:33:17 +01:00
// Check if the claimed email address is available for use.
2021-08-25 14:34:33 +01:00
emailAvailable , err := m . db . IsEmailAvailable ( ctx , claims . Email )
if err != nil {
2023-07-23 11:33:17 +01:00
err := gtserror . Newf ( "db error checking email availability: %w" , err )
return nil , gtserror . NewErrorInternalError ( err )
2021-07-23 09:36:28 +01:00
}
2023-07-23 11:33:17 +01:00
if ! emailAvailable {
const 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"
err := gtserror . Newf ( "email address %s is not available" , claims . Email )
return nil , gtserror . NewErrorConflict ( err , help )
2021-07-23 09:36:28 +01:00
}
2024-02-27 15:07:29 +00:00
if ! allowedGroup ( claims . Groups ) {
err := fmt . Errorf ( "User groups %+v do not include an allowed group" , claims . Groups )
return nil , gtserror . NewErrorUnauthorized ( err , err . Error ( ) )
}
2023-07-23 11:33:17 +01:00
// We still need to set something as a password, even
// if it's not a password the user will end up using.
//
// We'll just set two uuids on top of each other, which
// should be long + random enough to baffle any attempts
// to crack, and which is also, conveniently, 72 bytes,
// which is the maximum length that bcrypt can handle.
2021-07-23 09:36:28 +01:00
//
2023-07-23 11:33:17 +01:00
// If the user ever wants to log in using a 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 ( )
2023-07-23 11:33:17 +01:00
// Since this user is created via OIDC, we can assume
// that the account should be preapproved, and the email
// address should be considered as verified already,
// since the OIDC login was successful.
2022-01-31 15:03:47 +00:00
//
2023-07-23 11:33:17 +01:00
// If we don't assume this, we end up in a situation
// where the admin first adds a user to OIDC, then has
// to approve them again in GoToSocial when they log in
// there for the first time, which doesn't make sense.
2022-01-31 15:03:47 +00:00
//
2023-07-23 11:33:17 +01:00
// In other words, if a user logs in via OIDC, they
// should be able to use their account straight away.
var (
preApproved = true
emailVerified = true
)
// If one of the claimed groups corresponds to one of
// the configured admin OIDC groups, create this user
// as an admin.
admin := adminGroup ( claims . Groups )
// Create the user! This will also create an account and
// store it in the database, so we don't need to do that.
user , err := m . db . NewSignup ( ctx , gtsmodel . NewSignup {
Username : extraInfo . Username ,
Email : claims . Email ,
Password : password ,
SignUpIP : ip ,
AppID : appID ,
ExternalID : claims . Sub ,
PreApproved : preApproved ,
EmailVerified : emailVerified ,
Admin : admin ,
} )
2021-07-23 09:36:28 +01:00
if err != nil {
2023-07-23 11:33:17 +01:00
err := gtserror . Newf ( "db error doing new signup: %w" , err )
2022-06-08 19:38:03 +01:00
return nil , gtserror . NewErrorInternalError ( err )
2021-07-23 09:36:28 +01:00
}
return user , nil
}
2023-07-23 11:33:17 +01:00
// adminGroup returns true if one of the given OIDC
// groups is equal to at least one admin OIDC group.
func adminGroup ( groups [ ] string ) bool {
2024-02-27 15:07:29 +00:00
adminGroups := config . GetOIDCAdminGroups ( )
for _ , claimedGroup := range groups {
if slices . ContainsFunc ( adminGroups , func ( allowedGroup string ) bool {
return strings . EqualFold ( claimedGroup , allowedGroup )
} ) {
return true
2023-07-23 11:33:17 +01:00
}
}
// User is in no admin groups,
// ∴ user is not an admin.
return false
}
2024-02-27 15:07:29 +00:00
// allowedGroup returns true if one of the given OIDC
// groups is equal to at least one allowed OIDC group.
func allowedGroup ( groups [ ] string ) bool {
allowedGroups := config . GetOIDCAllowedGroups ( )
if len ( allowedGroups ) == 0 {
// If no groups are configured, allow access (for backwards compatibility)
return true
}
for _ , claimedGroup := range groups {
if slices . ContainsFunc ( allowedGroups , func ( allowedGroup string ) bool {
return strings . EqualFold ( claimedGroup , allowedGroup )
} ) {
return true
}
}
// User is in no allowed groups,
// ∴ user is not allowed to log in
return false
}