| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | /* | 
					
						
							|  |  |  |    GoToSocial | 
					
						
							|  |  |  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |    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/>. | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package auth | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/gin-contrib/sessions" | 
					
						
							|  |  |  | 	"github.com/gin-gonic/gin" | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 	"github.com/google/uuid" | 
					
						
							| 
									
										
										
										
											2021-05-08 14:25:55 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | 
					
						
							| 
									
										
										
										
											2021-05-21 15:48:26 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | 
					
						
							| 
									
										
										
										
											2021-05-08 14:25:55 +02:00
										 |  |  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-20 18:14:23 +02:00
										 |  |  | // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | // The idea here is to present an oauth authorize page to the user, with a button | 
					
						
							|  |  |  | // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user | 
					
						
							| 
									
										
										
										
											2021-04-20 18:14:23 +02:00
										 |  |  | func (m *Module) AuthorizeGETHandler(c *gin.Context) { | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	l := m.log.WithField("func", "AuthorizeGETHandler") | 
					
						
							|  |  |  | 	s := sessions.Default(c) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// UserID will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow | 
					
						
							|  |  |  | 	// 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. | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	userID, ok := s.Get(sessionUserID).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || userID == "" { | 
					
						
							|  |  |  | 		l.Trace("userid was empty, parsing form then redirecting to sign in page") | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 		form := &model.OAuthAuthorize{} | 
					
						
							|  |  |  | 		if err := c.Bind(form); err != nil { | 
					
						
							|  |  |  | 			l.Debugf("invalid auth form: %s", err) | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 			m.clearSession(s) | 
					
						
							|  |  |  | 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		l.Debugf("parsed auth form: %+v", form) | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if err := extractAuthForm(s, form); err != nil { | 
					
						
							|  |  |  | 			l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err)) | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 			m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 			return | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		c.Redirect(http.StatusSeeOther, AuthSignInPath) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// We can use the client_id on the session to retrieve info about the app associated with the client_id | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	clientID, ok := s.Get(sessionClientID).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || clientID == "" { | 
					
						
							|  |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session"}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-08-25 15:34:33 +02:00
										 |  |  | 	app := >smodel.Application{} | 
					
						
							|  |  |  | 	if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: app.ClientID}}, app); err != nil { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// we can also use the userid of the user to fetch their username from the db to greet them nicely <3 | 
					
						
							| 
									
										
										
										
											2021-08-25 15:34:33 +02:00
										 |  |  | 	user := >smodel.User{} | 
					
						
							|  |  |  | 	if err := m.db.GetByID(c.Request.Context(), user.ID, user); err != nil { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-25 15:34:33 +02:00
										 |  |  | 	acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Finally we should also get the redirect and scope of this particular request, as stored in the session. | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	redirect, ok := s.Get(sessionRedirectURI).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || redirect == "" { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": "no redirect_uri found in session"}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	scope, ok := s.Get(sessionScope).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || scope == "" { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		m.clearSession(s) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		c.JSON(http.StatusInternalServerError, gin.H{"error": "no scope found in session"}) | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// the authorize template will display a form to the user where they can get some information | 
					
						
							|  |  |  | 	// about the app that's trying to authorize, and the scope of the request. | 
					
						
							|  |  |  | 	// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler | 
					
						
							|  |  |  | 	l.Trace("serving authorize html") | 
					
						
							|  |  |  | 	c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ | 
					
						
							|  |  |  | 		"appname":    app.Name, | 
					
						
							|  |  |  | 		"appwebsite": app.Website, | 
					
						
							|  |  |  | 		"redirect":   redirect, | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 		sessionScope: scope, | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 		"user":       acct.Username, | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-20 18:14:23 +02:00
										 |  |  | // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | // At this point we assume that the user has A) logged in and B) accepted that the app should act for them, | 
					
						
							|  |  |  | // so we should proceed with the authentication flow and generate an oauth token for them if we can. | 
					
						
							|  |  |  | // See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user | 
					
						
							| 
									
										
										
										
											2021-04-20 18:14:23 +02:00
										 |  |  | func (m *Module) AuthorizePOSTHandler(c *gin.Context) { | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	l := m.log.WithField("func", "AuthorizePOSTHandler") | 
					
						
							|  |  |  | 	s := sessions.Default(c) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// We need to retrieve the original form submitted to the authorizeGEThandler, and | 
					
						
							|  |  |  | 	// recreate it on the request so that it can be used further by the oauth2 library. | 
					
						
							|  |  |  | 	// So first fetch all the values from the session. | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 	errs := []string{} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	forceLogin, ok := s.Get(sessionForceLogin).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		forceLogin = "false" | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	responseType, ok := s.Get(sessionResponseType).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || responseType == "" { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		errs = append(errs, "session missing response_type") | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	clientID, ok := s.Get(sessionClientID).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || clientID == "" { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		errs = append(errs, "session missing client_id") | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	redirectURI, ok := s.Get(sessionRedirectURI).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok || redirectURI == "" { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		errs = append(errs, "session missing redirect_uri") | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	scope, ok := s.Get(sessionScope).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		errs = append(errs, "session missing scope") | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	userID, ok := s.Get(sessionUserID).(string) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	if !ok { | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 		errs = append(errs, "session missing userid") | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-07-07 15:46:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 	m.clearSession(s) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if len(errs) != 0 { | 
					
						
							|  |  |  | 		c.JSON(http.StatusBadRequest, gin.H{"error": strings.Join(errs, ": ")}) | 
					
						
							| 
									
										
										
										
											2021-07-07 15:46:42 +02:00
										 |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// now set the values on the request | 
					
						
							|  |  |  | 	values := url.Values{} | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	values.Set(sessionForceLogin, forceLogin) | 
					
						
							|  |  |  | 	values.Set(sessionResponseType, responseType) | 
					
						
							|  |  |  | 	values.Set(sessionClientID, clientID) | 
					
						
							|  |  |  | 	values.Set(sessionRedirectURI, redirectURI) | 
					
						
							|  |  |  | 	values.Set(sessionScope, scope) | 
					
						
							|  |  |  | 	values.Set(sessionUserID, userID) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	c.Request.Form = values | 
					
						
							|  |  |  | 	l.Tracef("values on request set to %+v", c.Request.Form) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// and proceed with authorization using the oauth2 library | 
					
						
							|  |  |  | 	if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { | 
					
						
							|  |  |  | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | // extractAuthForm checks the given OAuthAuthorize form, and stores | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | // the values in the form into the session. | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error { | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	// these fields are *required* so check 'em | 
					
						
							|  |  |  | 	if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { | 
					
						
							|  |  |  | 		return errors.New("missing one of: response_type, client_id or redirect_uri") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// set default scope to read | 
					
						
							|  |  |  | 	if form.Scope == "" { | 
					
						
							|  |  |  | 		form.Scope = "read" | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// save these values from the form so we can use them elsewhere in the session | 
					
						
							| 
									
										
										
										
											2021-07-08 11:32:31 +02:00
										 |  |  | 	s.Set(sessionForceLogin, form.ForceLogin) | 
					
						
							|  |  |  | 	s.Set(sessionResponseType, form.ResponseType) | 
					
						
							|  |  |  | 	s.Set(sessionClientID, form.ClientID) | 
					
						
							|  |  |  | 	s.Set(sessionRedirectURI, form.RedirectURI) | 
					
						
							|  |  |  | 	s.Set(sessionScope, form.Scope) | 
					
						
							| 
									
										
										
										
											2021-07-23 10:36:28 +02:00
										 |  |  | 	s.Set(sessionState, uuid.NewString()) | 
					
						
							| 
									
										
										
										
											2021-04-01 20:46:45 +02:00
										 |  |  | 	return s.Save() | 
					
						
							|  |  |  | } |