mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
Currently authentication methods return information in two forms: they return who was authenticated as a `*user_model.User`, and then they insert key-values into `ctx.Data` which has critical impact on how the authenticated request is treated. This PR changes the authentication methods to return structured data in the form of an `AuthenticationResult`, with all the key-value information in `ctx.Data` being moved into methods on the `AuthenticationResult` interface. Authentication workflows in Forgejo are a real mess. This is the first step in trying to clean it up and make the code predictable and reasonable, and is both follow-up work that was identified from the repo-specific access tokens (where the `"ApiTokenReducer"` key-value was added), and is pre-requisite work to future JWT enhancements that are [being discussed](https://codeberg.org/forgejo/forgejo/issues/3571#issuecomment-13268004). ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - All changes, at least in theory, are refactors of existing logic and are not expected to have functional deviations -- existing regression tests are the only planned testing. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12202 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
284 lines
9.6 KiB
Go
284 lines
9.6 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"forgejo.org/models/auth"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/util"
|
|
"forgejo.org/modules/web"
|
|
auth_method "forgejo.org/services/auth/method"
|
|
"forgejo.org/services/auth/source/oauth2"
|
|
"forgejo.org/services/context"
|
|
"forgejo.org/services/externalaccount"
|
|
"forgejo.org/services/forms"
|
|
|
|
"github.com/markbates/goth"
|
|
)
|
|
|
|
var tplLinkAccount base.TplName = "user/auth/link_account"
|
|
|
|
// LinkAccount shows the page where the user can decide to login or create a new account
|
|
func LinkAccount(ctx *context.Context) {
|
|
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
|
ctx.Data["Title"] = ctx.Tr("link_account")
|
|
ctx.Data["LinkAccountMode"] = true
|
|
if setting.Service.RequireExternalRegistrationCaptcha {
|
|
context.SetCaptchaData(ctx)
|
|
}
|
|
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
|
|
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
|
ctx.Data["ShowRegistrationButton"] = false
|
|
ctx.Data["EnableInternalSignIn"] = true
|
|
|
|
// use this to set the right link into the signIn and signUp templates in the link_account template
|
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
|
|
|
gothUser := ctx.Session.Get("linkAccountGothUser")
|
|
if gothUser == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
|
|
return
|
|
}
|
|
|
|
gu, _ := gothUser.(goth.User)
|
|
uname, err := getUserName(&gu)
|
|
if err != nil {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
email := gu.Email
|
|
ctx.Data["user_name"] = uname
|
|
ctx.Data["email"] = email
|
|
|
|
if len(email) != 0 {
|
|
u, err := user_model.GetUserByEmail(ctx, email)
|
|
if err != nil && !user_model.IsErrUserNotExist(err) {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
if u != nil {
|
|
ctx.Data["user_exists"] = true
|
|
}
|
|
} else if len(uname) != 0 {
|
|
u, err := user_model.GetUserByName(ctx, uname)
|
|
if err != nil && !user_model.IsErrUserNotExist(err) {
|
|
ctx.ServerError("UserSignIn", err)
|
|
return
|
|
}
|
|
if u != nil {
|
|
ctx.Data["user_exists"] = true
|
|
}
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, tplLinkAccount)
|
|
}
|
|
|
|
func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl base.TplName, invoker string, err error) {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
|
} else if errors.Is(err, util.ErrInvalidArgument) {
|
|
ctx.Data["user_exists"] = true
|
|
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
|
} else if user_model.IsErrUserProhibitLogin(err) {
|
|
ctx.Data["user_exists"] = true
|
|
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
|
} else {
|
|
ctx.ServerError(invoker, err)
|
|
}
|
|
}
|
|
|
|
// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
|
|
func LinkAccountPostSignIn(ctx *context.Context) {
|
|
signInForm := web.GetForm(ctx).(*forms.SignInForm)
|
|
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
|
ctx.Data["Title"] = ctx.Tr("link_account")
|
|
ctx.Data["LinkAccountMode"] = true
|
|
ctx.Data["LinkAccountModeSignIn"] = true
|
|
if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
|
|
context.SetCaptchaData(ctx)
|
|
}
|
|
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
|
|
ctx.Data["ShowRegistrationButton"] = false
|
|
ctx.Data["EnableInternalSignIn"] = true
|
|
|
|
// use this to set the right link into the signIn and signUp templates in the link_account template
|
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
|
|
|
gothUser := ctx.Session.Get("linkAccountGothUser")
|
|
if gothUser == nil {
|
|
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
|
|
return
|
|
}
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplLinkAccount)
|
|
return
|
|
}
|
|
|
|
u, _, err := auth_method.UserSignIn(ctx, signInForm.UserName, signInForm.Password)
|
|
if err != nil {
|
|
handleSignInError(ctx, signInForm.UserName, &signInForm, tplLinkAccount, "UserLinkAccount", err)
|
|
return
|
|
}
|
|
|
|
linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
|
|
}
|
|
|
|
func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
|
|
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
|
|
|
|
// If this user is enrolled in 2FA, we can't sign the user in just yet.
|
|
// Instead, redirect them to the 2FA authentication page.
|
|
// We deliberately ignore the skip local 2fa setting here because we are linking to a previous user here
|
|
hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
|
|
if err != nil {
|
|
ctx.ServerError("HasTwoFactorByUID", err)
|
|
return
|
|
}
|
|
|
|
if !hasTwoFactor {
|
|
if err := externalaccount.LinkAccountToUser(ctx, u, gothUser); err != nil {
|
|
ctx.ServerError("UserLinkAccount", err)
|
|
return
|
|
}
|
|
|
|
handleSignIn(ctx, u, remember)
|
|
return
|
|
}
|
|
|
|
if err := updateSession(ctx, nil, map[string]any{
|
|
// User needs to use 2FA, save data and redirect to 2FA page.
|
|
"twofaUid": u.ID,
|
|
"twofaRemember": remember,
|
|
"linkAccount": true,
|
|
}); err != nil {
|
|
ctx.ServerError("RegenerateSession", err)
|
|
return
|
|
}
|
|
|
|
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
|
|
regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
|
|
if err == nil && len(regs) > 0 {
|
|
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
|
|
return
|
|
}
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
|
}
|
|
|
|
// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
|
|
func LinkAccountPostRegister(ctx *context.Context) {
|
|
form := web.GetForm(ctx).(*forms.RegisterForm)
|
|
// TODO Make insecure passwords optional for local accounts also,
|
|
// once email-based Second-Factor Auth is available
|
|
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
|
ctx.Data["Title"] = ctx.Tr("link_account")
|
|
ctx.Data["LinkAccountMode"] = true
|
|
ctx.Data["LinkAccountModeRegister"] = true
|
|
if setting.Service.RequireExternalRegistrationCaptcha {
|
|
context.SetCaptchaData(ctx)
|
|
}
|
|
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
|
|
ctx.Data["ShowRegistrationButton"] = false
|
|
|
|
// use this to set the right link into the signIn and signUp templates in the link_account template
|
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
|
|
|
gothUserInterface := ctx.Session.Get("linkAccountGothUser")
|
|
if gothUserInterface == nil {
|
|
ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
|
|
return
|
|
}
|
|
gothUser, ok := gothUserInterface.(goth.User)
|
|
if !ok {
|
|
ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
|
|
return
|
|
}
|
|
|
|
if ctx.HasError() {
|
|
ctx.HTML(http.StatusOK, tplLinkAccount)
|
|
return
|
|
}
|
|
|
|
if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
|
|
ctx.Error(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
|
|
context.VerifyCaptcha(ctx, tplLinkAccount, form)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
}
|
|
|
|
if emailValid, ok := form.IsEmailDomainAllowed(); !emailValid {
|
|
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSignUp, form)
|
|
return
|
|
} else if !ok {
|
|
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
|
|
return
|
|
}
|
|
|
|
if setting.Service.AllowOnlyExternalRegistration || !setting.Service.RequireExternalRegistrationPassword {
|
|
// In user_model.User an empty password is classed as not set, so we set form.Password to empty.
|
|
// Eventually the database should be changed to indicate "Second Factor"-enabled accounts
|
|
// (accounts that do not introduce the security vulnerabilities of a password).
|
|
// If a user decides to circumvent second-factor security, and purposefully create a password,
|
|
// they can still do so using the "Recover Account" option.
|
|
form.Password = ""
|
|
} else {
|
|
if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
|
|
ctx.Data["Err_Password"] = true
|
|
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
|
|
return
|
|
}
|
|
if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
|
|
ctx.Data["Err_Password"] = true
|
|
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
|
|
return
|
|
}
|
|
}
|
|
|
|
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
|
|
if err != nil {
|
|
ctx.ServerError("CreateUser", err)
|
|
return
|
|
}
|
|
|
|
u := &user_model.User{
|
|
Name: form.UserName,
|
|
Email: form.Email,
|
|
Passwd: form.Password,
|
|
LoginType: auth.OAuth2,
|
|
LoginSource: authSource.ID,
|
|
LoginName: gothUser.UserID,
|
|
}
|
|
|
|
if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
|
|
// error already handled
|
|
return
|
|
}
|
|
|
|
source := authSource.Cfg.(*oauth2.Source)
|
|
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
|
|
ctx.ServerError("SyncGroupsToTeams", err)
|
|
return
|
|
}
|
|
|
|
handleSignIn(ctx, u, false)
|
|
}
|