gtsocial-umbx

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

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 }