authorize.go (11667B)
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 "errors" 22 "fmt" 23 "net/http" 24 "net/url" 25 26 "github.com/gin-contrib/sessions" 27 "github.com/gin-gonic/gin" 28 "github.com/google/uuid" 29 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 30 apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" 31 "github.com/superseriousbusiness/gotosocial/internal/db" 32 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 33 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 34 "github.com/superseriousbusiness/gotosocial/internal/oauth" 35 ) 36 37 // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize 38 // The idea here is to present an oauth authorize page to the user, with a button 39 // that they have to click to accept. 40 func (m *Module) AuthorizeGETHandler(c *gin.Context) { 41 s := sessions.Default(c) 42 43 if _, err := apiutil.NegotiateAccept(c, apiutil.HTMLAcceptHeaders...); err != nil { 44 apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) 45 return 46 } 47 48 // UserID will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow 49 // 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. 50 userID, ok := s.Get(sessionUserID).(string) 51 if !ok || userID == "" { 52 form := &apimodel.OAuthAuthorize{} 53 if err := c.ShouldBind(form); err != nil { 54 m.clearSession(s) 55 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 56 return 57 } 58 59 if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil { 60 m.clearSession(s) 61 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 62 return 63 } 64 65 c.Redirect(http.StatusSeeOther, "/auth"+AuthSignInPath) 66 return 67 } 68 69 // use session information to validate app, user, and account for this request 70 clientID, ok := s.Get(sessionClientID).(string) 71 if !ok || clientID == "" { 72 m.clearSession(s) 73 err := fmt.Errorf("key %s was not found in session", sessionClientID) 74 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 75 return 76 } 77 78 app := >smodel.Application{} 79 if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil { 80 m.clearSession(s) 81 safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID) 82 var errWithCode gtserror.WithCode 83 if err == db.ErrNoEntries { 84 errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice) 85 } else { 86 errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) 87 } 88 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 89 return 90 } 91 92 user, err := m.db.GetUserByID(c.Request.Context(), userID) 93 if err != nil { 94 m.clearSession(s) 95 safe := fmt.Sprintf("user with id %s could not be retrieved", userID) 96 var errWithCode gtserror.WithCode 97 if err == db.ErrNoEntries { 98 errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice) 99 } else { 100 errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) 101 } 102 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 103 return 104 } 105 106 acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) 107 if err != nil { 108 m.clearSession(s) 109 safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) 110 var errWithCode gtserror.WithCode 111 if err == db.ErrNoEntries { 112 errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice) 113 } else { 114 errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) 115 } 116 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 117 return 118 } 119 120 if ensureUserIsAuthorizedOrRedirect(c, user, acct) { 121 return 122 } 123 124 // Finally we should also get the redirect and scope of this particular request, as stored in the session. 125 redirect, ok := s.Get(sessionRedirectURI).(string) 126 if !ok || redirect == "" { 127 m.clearSession(s) 128 err := fmt.Errorf("key %s was not found in session", sessionRedirectURI) 129 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 130 return 131 } 132 133 scope, ok := s.Get(sessionScope).(string) 134 if !ok || scope == "" { 135 m.clearSession(s) 136 err := fmt.Errorf("key %s was not found in session", sessionScope) 137 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 138 return 139 } 140 141 instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) 142 if errWithCode != nil { 143 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 144 return 145 } 146 147 // the authorize template will display a form to the user where they can get some information 148 // about the app that's trying to authorize, and the scope of the request. 149 // They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler 150 c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ 151 "appname": app.Name, 152 "appwebsite": app.Website, 153 "redirect": redirect, 154 "scope": scope, 155 "user": acct.Username, 156 "instance": instance, 157 }) 158 } 159 160 // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize 161 // At this point we assume that the user has A) logged in and B) accepted that the app should act for them, 162 // so we should proceed with the authentication flow and generate an oauth token for them if we can. 163 func (m *Module) AuthorizePOSTHandler(c *gin.Context) { 164 s := sessions.Default(c) 165 166 // We need to retrieve the original form submitted to the authorizeGEThandler, and 167 // recreate it on the request so that it can be used further by the oauth2 library. 168 errs := []string{} 169 170 forceLogin, ok := s.Get(sessionForceLogin).(string) 171 if !ok { 172 forceLogin = "false" 173 } 174 175 responseType, ok := s.Get(sessionResponseType).(string) 176 if !ok || responseType == "" { 177 errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType)) 178 } 179 180 clientID, ok := s.Get(sessionClientID).(string) 181 if !ok || clientID == "" { 182 errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID)) 183 } 184 185 redirectURI, ok := s.Get(sessionRedirectURI).(string) 186 if !ok || redirectURI == "" { 187 errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI)) 188 } 189 190 scope, ok := s.Get(sessionScope).(string) 191 if !ok { 192 errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope)) 193 } 194 195 var clientState string 196 if s, ok := s.Get(sessionClientState).(string); ok { 197 clientState = s 198 } 199 200 userID, ok := s.Get(sessionUserID).(string) 201 if !ok { 202 errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID)) 203 } 204 205 if len(errs) != 0 { 206 errs = append(errs, oauth.HelpfulAdvice) 207 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGetV1) 208 return 209 } 210 211 user, err := m.db.GetUserByID(c.Request.Context(), userID) 212 if err != nil { 213 m.clearSession(s) 214 safe := fmt.Sprintf("user with id %s could not be retrieved", userID) 215 var errWithCode gtserror.WithCode 216 if err == db.ErrNoEntries { 217 errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice) 218 } else { 219 errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) 220 } 221 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 222 return 223 } 224 225 acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) 226 if err != nil { 227 m.clearSession(s) 228 safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID) 229 var errWithCode gtserror.WithCode 230 if err == db.ErrNoEntries { 231 errWithCode = gtserror.NewErrorBadRequest(err, safe, oauth.HelpfulAdvice) 232 } else { 233 errWithCode = gtserror.NewErrorInternalError(err, safe, oauth.HelpfulAdvice) 234 } 235 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 236 return 237 } 238 239 if ensureUserIsAuthorizedOrRedirect(c, user, acct) { 240 return 241 } 242 243 if redirectURI != oauth.OOBURI { 244 // we're done with the session now, so just clear it out 245 m.clearSession(s) 246 } 247 248 // we have to set the values on the request form 249 // so that they're picked up by the oauth server 250 c.Request.Form = url.Values{ 251 sessionForceLogin: {forceLogin}, 252 sessionResponseType: {responseType}, 253 sessionClientID: {clientID}, 254 sessionRedirectURI: {redirectURI}, 255 sessionScope: {scope}, 256 sessionUserID: {userID}, 257 } 258 259 if clientState != "" { 260 c.Request.Form.Set("state", clientState) 261 } 262 263 if errWithCode := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); errWithCode != nil { 264 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 265 } 266 } 267 268 // saveAuthFormToSession checks the given OAuthAuthorize form, 269 // and stores the values in the form into the session. 270 func saveAuthFormToSession(s sessions.Session, form *apimodel.OAuthAuthorize) gtserror.WithCode { 271 if form == nil { 272 err := errors.New("OAuthAuthorize form was nil") 273 return gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice) 274 } 275 276 if form.ResponseType == "" { 277 err := errors.New("field response_type was not set on OAuthAuthorize form") 278 return gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice) 279 } 280 281 if form.ClientID == "" { 282 err := errors.New("field client_id was not set on OAuthAuthorize form") 283 return gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice) 284 } 285 286 if form.RedirectURI == "" { 287 err := errors.New("field redirect_uri was not set on OAuthAuthorize form") 288 return gtserror.NewErrorBadRequest(err, err.Error(), oauth.HelpfulAdvice) 289 } 290 291 // set default scope to read 292 if form.Scope == "" { 293 form.Scope = "read" 294 } 295 296 // save these values from the form so we can use them elsewhere in the session 297 s.Set(sessionForceLogin, form.ForceLogin) 298 s.Set(sessionResponseType, form.ResponseType) 299 s.Set(sessionClientID, form.ClientID) 300 s.Set(sessionRedirectURI, form.RedirectURI) 301 s.Set(sessionScope, form.Scope) 302 s.Set(sessionInternalState, uuid.NewString()) 303 s.Set(sessionClientState, form.State) 304 305 if err := s.Save(); err != nil { 306 err := fmt.Errorf("error saving form values onto session: %s", err) 307 return gtserror.NewErrorInternalError(err, oauth.HelpfulAdvice) 308 } 309 310 return nil 311 } 312 313 func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) { 314 if user.ConfirmedAt.IsZero() { 315 ctx.Redirect(http.StatusSeeOther, "/auth"+AuthCheckYourEmailPath) 316 redirected = true 317 return 318 } 319 320 if !*user.Approved { 321 ctx.Redirect(http.StatusSeeOther, "/auth"+AuthWaitForApprovalPath) 322 redirected = true 323 return 324 } 325 326 if *user.Disabled || !account.SuspendedAt.IsZero() { 327 ctx.Redirect(http.StatusSeeOther, "/auth"+AuthAccountDisabledPath) 328 redirected = true 329 return 330 } 331 332 return 333 }