signin.go (5585B)
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/http" 25 26 "github.com/gin-contrib/sessions" 27 "github.com/gin-gonic/gin" 28 apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" 29 "github.com/superseriousbusiness/gotosocial/internal/config" 30 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 31 "github.com/superseriousbusiness/gotosocial/internal/oauth" 32 "golang.org/x/crypto/bcrypt" 33 ) 34 35 // login just wraps a form-submitted username (we want an email) and password 36 type login struct { 37 Email string `form:"username"` 38 Password string `form:"password"` 39 } 40 41 // SignInGETHandler should be served at https://example.org/auth/sign_in. 42 // The idea is to present a sign in page to the user, where they can enter their username and password. 43 // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler. 44 // If an idp provider is set, then the user will be redirected to that to do their sign in. 45 func (m *Module) SignInGETHandler(c *gin.Context) { 46 if _, err := apiutil.NegotiateAccept(c, apiutil.HTMLAcceptHeaders...); err != nil { 47 apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1) 48 return 49 } 50 51 if !config.GetOIDCEnabled() { 52 instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) 53 if errWithCode != nil { 54 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 55 return 56 } 57 58 // no idp provider, use our own funky little sign in page 59 c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{ 60 "instance": instance, 61 }) 62 return 63 } 64 65 // idp provider is in use, so redirect to it 66 s := sessions.Default(c) 67 68 internalStateI := s.Get(sessionInternalState) 69 internalState, ok := internalStateI.(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 c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(internalState)) 78 } 79 80 // SignInPOSTHandler should be served at https://example.org/auth/sign_in. 81 // The idea is to present a sign in page to the user, where they can enter their username and password. 82 // The handler will then redirect to the auth handler served at /auth 83 func (m *Module) SignInPOSTHandler(c *gin.Context) { 84 s := sessions.Default(c) 85 86 form := &login{} 87 if err := c.ShouldBind(form); err != nil { 88 m.clearSession(s) 89 apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 90 return 91 } 92 93 userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password) 94 if errWithCode != nil { 95 // don't clear session here, so the user can just press back and try again 96 // if they accidentally gave the wrong password or something 97 apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) 98 return 99 } 100 101 s.Set(sessionUserID, userid) 102 if err := s.Save(); err != nil { 103 err := fmt.Errorf("error saving user id onto session: %s", err) 104 apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err, oauth.HelpfulAdvice), m.processor.InstanceGetV1) 105 } 106 107 c.Redirect(http.StatusFound, "/oauth"+OauthAuthorizePath) 108 } 109 110 // ValidatePassword takes an email address and a password. 111 // The goal is to authenticate the password against the one for that email 112 // address stored in the database. If OK, we return the userid (a ulid) for that user, 113 // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. 114 func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) { 115 if email == "" || password == "" { 116 err := errors.New("email or password was not provided") 117 return incorrectPassword(err) 118 } 119 120 user, err := m.db.GetUserByEmailAddress(ctx, email) 121 if err != nil { 122 err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) 123 return incorrectPassword(err) 124 } 125 126 if user.EncryptedPassword == "" { 127 err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email) 128 return incorrectPassword(err) 129 } 130 131 if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil { 132 err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err) 133 return incorrectPassword(err) 134 } 135 136 return user.ID, nil 137 } 138 139 // incorrectPassword wraps the given error in a gtserror.WithCode, and returns 140 // only a generic 'safe' error message to the user, to not give any info away. 141 func incorrectPassword(err error) (string, gtserror.WithCode) { 142 safeErr := fmt.Errorf("password/email combination was incorrect") 143 return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), oauth.HelpfulAdvice) 144 }