gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

callback.go (12128B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package auth
     19 
     20 import (
     21 	"context"
     22 	"errors"
     23 	"fmt"
     24 	"net"
     25 	"net/http"
     26 	"strings"
     27 
     28 	"github.com/gin-contrib/sessions"
     29 	"github.com/gin-gonic/gin"
     30 	"github.com/google/uuid"
     31 	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
     32 	"github.com/superseriousbusiness/gotosocial/internal/config"
     33 	"github.com/superseriousbusiness/gotosocial/internal/db"
     34 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     35 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     36 	"github.com/superseriousbusiness/gotosocial/internal/oauth"
     37 	"github.com/superseriousbusiness/gotosocial/internal/oidc"
     38 	"github.com/superseriousbusiness/gotosocial/internal/validate"
     39 )
     40 
     41 // extraInfo wraps a form-submitted username and transmitted name
     42 type extraInfo struct {
     43 	Username string `form:"username"`
     44 	Name     string `form:"name"` // note that this is only used for re-rendering the page in case of an error
     45 }
     46 
     47 // CallbackGETHandler parses a token from an external auth provider.
     48 func (m *Module) CallbackGETHandler(c *gin.Context) {
     49 	if !config.GetOIDCEnabled() {
     50 		err := errors.New("oidc is not enabled for this server")
     51 		apiutil.ErrorHandler(c, gtserror.NewErrorNotFound(err, err.Error()), m.processor.InstanceGetV1)
     52 		return
     53 	}
     54 
     55 	s := sessions.Default(c)
     56 
     57 	// check the query vs session state parameter to mitigate csrf
     58 	// https://auth0.com/docs/secure/attack-protection/state-parameters
     59 
     60 	returnedInternalState := c.Query(callbackStateParam)
     61 	if returnedInternalState == "" {
     62 		m.clearSession(s)
     63 		err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam)
     64 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
     65 		return
     66 	}
     67 
     68 	savedInternalStateI := s.Get(sessionInternalState)
     69 	savedInternalState, ok := savedInternalStateI.(string)
     70 	if !ok {
     71 		m.clearSession(s)
     72 		err := fmt.Errorf("key %s was not found in session", sessionInternalState)
     73 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
     74 		return
     75 	}
     76 
     77 	if returnedInternalState != savedInternalState {
     78 		m.clearSession(s)
     79 		err := errors.New("mismatch between callback state and saved state")
     80 		apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
     81 		return
     82 	}
     83 
     84 	// retrieve stored claims using code
     85 	code := c.Query(callbackCodeParam)
     86 	if code == "" {
     87 		m.clearSession(s)
     88 		err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam)
     89 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
     90 		return
     91 	}
     92 
     93 	claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code)
     94 	if errWithCode != nil {
     95 		m.clearSession(s)
     96 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
     97 		return
     98 	}
     99 
    100 	// We can use the client_id on the session to retrieve
    101 	// info about the app associated with the client_id
    102 	clientID, ok := s.Get(sessionClientID).(string)
    103 	if !ok || clientID == "" {
    104 		m.clearSession(s)
    105 		err := fmt.Errorf("key %s was not found in session", sessionClientID)
    106 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
    107 		return
    108 	}
    109 
    110 	app := &gtsmodel.Application{}
    111 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
    112 		m.clearSession(s)
    113 		safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
    114 		var errWithCode gtserror.WithCode
    115 		if err == db.ErrNoEntries {
    116 			errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice)
    117 		} else {
    118 			errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice)
    119 		}
    120 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    121 		return
    122 	}
    123 
    124 	user, errWithCode := m.fetchUserForClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
    125 	if errWithCode != nil {
    126 		m.clearSession(s)
    127 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    128 		return
    129 	}
    130 	if user == nil {
    131 		// no user exists yet - let's ask them for their preferred username
    132 		instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
    133 		if errWithCode != nil {
    134 			apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    135 			return
    136 		}
    137 
    138 		// store the claims in the session - that way we know the user is authenticated when processing the form later
    139 		s.Set(sessionClaims, claims)
    140 		s.Set(sessionAppID, app.ID)
    141 		if err := s.Save(); err != nil {
    142 			m.clearSession(s)
    143 			apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
    144 			return
    145 		}
    146 		c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
    147 			"instance":          instance,
    148 			"name":              claims.Name,
    149 			"preferredUsername": claims.PreferredUsername,
    150 		})
    151 		return
    152 	}
    153 	s.Set(sessionUserID, user.ID)
    154 	if err := s.Save(); err != nil {
    155 		m.clearSession(s)
    156 		apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
    157 		return
    158 	}
    159 	c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath)
    160 }
    161 
    162 // FinalizePOSTHandler registers the user after additional data has been provided
    163 func (m *Module) FinalizePOSTHandler(c *gin.Context) {
    164 	s := sessions.Default(c)
    165 
    166 	form := &extraInfo{}
    167 	if err := c.ShouldBind(form); err != nil {
    168 		m.clearSession(s)
    169 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
    170 		return
    171 	}
    172 
    173 	// since we have multiple possible validation error, `validationError` is a shorthand for rendering them
    174 	validationError := func(err error) {
    175 		instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
    176 		if errWithCode != nil {
    177 			apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    178 			return
    179 		}
    180 		c.HTML(http.StatusOK, "finalize.tmpl", gin.H{
    181 			"instance":          instance,
    182 			"name":              form.Name,
    183 			"preferredUsername": form.Username,
    184 			"error":             err,
    185 		})
    186 	}
    187 
    188 	// check if the username conforms to the spec
    189 	if err := validate.Username(form.Username); err != nil {
    190 		validationError(err)
    191 		return
    192 	}
    193 
    194 	// see if the username is still available
    195 	usernameAvailable, err := m.db.IsUsernameAvailable(c.Request.Context(), form.Username)
    196 	if err != nil {
    197 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
    198 		return
    199 	}
    200 	if !usernameAvailable {
    201 		validationError(fmt.Errorf("Username %s is already taken", form.Username))
    202 		return
    203 	}
    204 
    205 	// retrieve the information previously set by the oidc logic
    206 	appID, ok := s.Get(sessionAppID).(string)
    207 	if !ok {
    208 		err := fmt.Errorf("key %s was not found in session", sessionAppID)
    209 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
    210 		return
    211 	}
    212 
    213 	// retrieve the claims returned by the IDP. Having this present means that we previously already verified these claims
    214 	claims, ok := s.Get(sessionClaims).(*oidc.Claims)
    215 	if !ok {
    216 		err := fmt.Errorf("key %s was not found in session", sessionClaims)
    217 		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1)
    218 		return
    219 	}
    220 
    221 	// we're now ready to actually create the user
    222 	user, errWithCode := m.createUserFromOIDC(c.Request.Context(), claims, form, net.IP(c.ClientIP()), appID)
    223 	if errWithCode != nil {
    224 		m.clearSession(s)
    225 		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
    226 		return
    227 	}
    228 	s.Delete(sessionClaims)
    229 	s.Delete(sessionAppID)
    230 	s.Set(sessionUserID, user.ID)
    231 	if err := s.Save(); err != nil {
    232 		m.clearSession(s)
    233 		apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGetV1)
    234 		return
    235 	}
    236 	c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath)
    237 }
    238 
    239 func (m *Module) fetchUserForClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
    240 	if claims.Sub == "" {
    241 		err := errors.New("no sub claim found - is your provider OIDC compliant?")
    242 		return nil, gtserror.NewErrorBadRequest(err, err.Error())
    243 	}
    244 	user, err := m.db.GetUserByExternalID(ctx, claims.Sub)
    245 	if err == nil {
    246 		return user, nil
    247 	}
    248 	if err != db.ErrNoEntries {
    249 		err := fmt.Errorf("error checking database for externalID %s: %s", claims.Sub, err)
    250 		return nil, gtserror.NewErrorInternalError(err)
    251 	}
    252 	if !config.GetOIDCLinkExisting() {
    253 		return nil, nil
    254 	}
    255 	// fallback to email if we want to link existing users
    256 	user, err = m.db.GetUserByEmailAddress(ctx, claims.Email)
    257 	if err == db.ErrNoEntries {
    258 		return nil, nil
    259 	} else if err != nil {
    260 		err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
    261 		return nil, gtserror.NewErrorInternalError(err)
    262 	}
    263 	// at this point we have found a matching user but still need to link the newly received external ID
    264 
    265 	user.ExternalID = claims.Sub
    266 	err = m.db.UpdateUser(ctx, user, "external_id")
    267 	if err != nil {
    268 		err := fmt.Errorf("error linking existing user %s: %s", claims.Email, err)
    269 		return nil, gtserror.NewErrorInternalError(err)
    270 	}
    271 	return user, nil
    272 }
    273 
    274 func (m *Module) createUserFromOIDC(ctx context.Context, claims *oidc.Claims, extraInfo *extraInfo, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
    275 	// check if the email address is available for use; if it's not there's nothing we can so
    276 	emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
    277 	if err != nil {
    278 		return nil, gtserror.NewErrorBadRequest(err)
    279 	}
    280 	if !emailAvailable {
    281 		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"
    282 		return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email), help)
    283 	}
    284 
    285 	// check if the user is in any recognised admin groups
    286 	adminGroups := config.GetOIDCAdminGroups()
    287 	var admin bool
    288 LOOP:
    289 	for _, g := range claims.Groups {
    290 		for _, ag := range adminGroups {
    291 			if strings.EqualFold(g, ag) {
    292 				admin = true
    293 				break LOOP
    294 			}
    295 		}
    296 	}
    297 
    298 	// We still need to set *a* password even if it's not a password the user will end up using, so set something random.
    299 	// We'll just set two uuids on top of each other, which should be long + random enough to baffle any attempts to crack.
    300 	//
    301 	// 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
    302 	password := uuid.NewString() + uuid.NewString()
    303 
    304 	// Since this user is created via oidc, which has been set up by the admin, we can assume that the account is already
    305 	// implicitly approved, and that the email address has already been verified: otherwise, we end up in situations where
    306 	// the admin first approves the user in OIDC, and then has to approve them again in GoToSocial, which doesn't make sense.
    307 	//
    308 	// In other words, if a user logs in via OIDC, they should be able to use their account straight away.
    309 	//
    310 	// See: https://github.com/superseriousbusiness/gotosocial/issues/357
    311 	requireApproval := false
    312 	emailVerified := true
    313 
    314 	// create the user! this will also create an account and store it in the database so we don't need to do that here
    315 	user, err := m.db.NewSignup(ctx, extraInfo.Username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, claims.Sub, admin)
    316 	if err != nil {
    317 		return nil, gtserror.NewErrorInternalError(err)
    318 	}
    319 
    320 	return user, nil
    321 }