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 := >smodel.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 }