gtsocial-umbx

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

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