jojo/services/auth/method/reverseproxy.go
Mathieu Fenniak 9f7533c1f1 refactor: clarify four different outputs that authentication methods provide (#12231)
#12202 began a refactor of Forgejo's authentication implementations by providing structured data on an authentication success.  However, error cases were maintained as-is in that refactor, leaving a complex situation: what does returning an error from an authentication method mean?; does it mean that the authentication failed, or that a server error occurred?  Can another authentication still be tried?

This PR changes authentication methods so that they can return one of four things:
- `AuthenticationSuccess` with an authentication result.
- `AuthenticationNotAttempted` which indicates that no credentials relevant for this authentication method were presented.  If every method returned `AuthenticationNotAttempted`, then you would have an unauthenticated access.
- `AuthenticationAttemptedIncorrectCredential` which indicates that credentials were present and failed validation -- a situation indicating a `401 Unauthorized`.
- `AuthenticationError` which indicates that an internal server error occurred and failed authentication -- indicating a `500 Internal Server Error`.

This paves the way for one more refactor coming next: `basic.go` and `oauth2.go` perform 3-4 different authentications each (access tokens, oauth JWTs, actions tokens, actions JWTs, and username/password).  With the capability to return these more precise responses, these authentication methods can be split up into separate logic that isn't intertwined together.

## 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.
  - Relying on existing test suite, with changes for any compile errors -- the next refactor will simplify the auth methods so that they can be unit tested easily.
- 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/12231
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2026-04-23 02:30:41 +02:00

182 lines
6.4 KiB
Go

// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package method
import (
"errors"
"fmt"
"net/http"
"strings"
user_model "forgejo.org/models/user"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/auth"
gouuid "github.com/google/uuid"
)
// Ensure the struct implements the interface.
var (
_ auth.Method = &ReverseProxy{}
)
// ReverseProxyMethodName is the constant name of the ReverseProxy authentication method
const ReverseProxyMethodName = "reverse_proxy"
// ReverseProxy implements the Auth interface, but actually relies on
// a reverse proxy for authentication of users.
// On successful authentication the proxy is expected to populate the username in the
// "setting.ReverseProxyAuthUser" header. Optionally it can also populate the email of the
// user in the "setting.ReverseProxyAuthEmail" header.
type ReverseProxy struct{}
// getUserName extracts the username from the "setting.ReverseProxyAuthUser" header
func (r *ReverseProxy) getUserName(req *http.Request) string {
return strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser))
}
// getUserFromAuthUser extracts the username from the "setting.ReverseProxyAuthUser" header
// of the request and returns the corresponding user object for that name.
// Verification of header data is not performed as it should have already been done by
// the reverse proxy.
// If a username is available in the "setting.ReverseProxyAuthUser" header an existing
// user object is returned (populated with username or email found in header).
// Returns nil if header is empty.
func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) {
username := r.getUserName(req)
if len(username) == 0 {
return nil, util.ErrNotExist
}
log.Trace("ReverseProxy Authorization: Found username: %s", username)
user, err := user_model.GetUserByName(req.Context(), username)
if err != nil {
if !user_model.IsErrUserNotExist(err) || !r.isAutoRegisterAllowed() {
log.Error("GetUserByName: %v", err)
return nil, err
}
user = r.newUser(req)
}
return user, nil
}
// getEmail extracts the email from the "setting.ReverseProxyAuthEmail" header
func (r *ReverseProxy) getEmail(req *http.Request) string {
return strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthEmail))
}
// getUserFromAuthEmail extracts the username from the "setting.ReverseProxyAuthEmail" header
// of the request and returns the corresponding user object for that email.
// Verification of header data is not performed as it should have already been done by
// the reverse proxy.
// If an email is available in the "setting.ReverseProxyAuthEmail" header an existing
// user object is returned (populated with the email found in header).
// Returns nil if header is empty or if "setting.EnableReverseProxyEmail" is disabled.
func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) (*user_model.User, error) {
if !setting.Service.EnableReverseProxyEmail {
return nil, util.ErrNotExist
}
email := r.getEmail(req)
if len(email) == 0 {
return nil, util.ErrNotExist
}
log.Trace("ReverseProxy Authorization: Found email: %s", email)
user, err := user_model.GetUserByEmail(req.Context(), email)
if err != nil {
// Do not allow auto-registration, we don't have a username here
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
}
return nil, err
}
return user, nil
}
// Verify attempts to load a user object based on headers sent by the reverse proxy.
// First it will attempt to load it based on the username (see docs for getUserFromAuthUser),
// and failing that it will attempt to load it based on the email (see docs for getUserFromAuthEmail).
// Returns nil if the headers are empty or the user is not found.
func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
user, err := r.getUserFromAuthUser(req)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return &auth.AuthenticationError{Error: fmt.Errorf("reverse proxy getUserFromAuthUser: %w", err)}
}
if user == nil {
user, err = r.getUserFromAuthEmail(req)
if user == nil {
if errors.Is(err, util.ErrNotExist) {
// Not attempted is returned when no HTTP headers were provided, which is the cases that ErrNotExist
// represents:
return &auth.AuthenticationNotAttempted{}
}
return &auth.AuthenticationAttemptedIncorrectCredential{Error: errors.New("user not found")}
}
}
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
if sess != nil && (sess.Get("uid") == nil || sess.Get("uid").(int64) != user.ID) {
handleSignIn(w, req, sess, user)
}
}
log.Trace("ReverseProxy Authorization: Logged in user %-v", user)
return &auth.AuthenticationSuccess{Result: &reverseProxyAuthenticationResult{user: user}}
}
// isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true
func (r *ReverseProxy) isAutoRegisterAllowed() bool {
return setting.Service.EnableReverseProxyAutoRegister
}
// newUser creates a new user object for the purpose of automatic registration
// and populates its name and email with the information present in request headers.
func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
username := r.getUserName(req)
if len(username) == 0 {
return nil
}
email := gouuid.New().String() + "@localhost"
if setting.Service.EnableReverseProxyEmail {
webAuthEmail := req.Header.Get(setting.ReverseProxyAuthEmail)
if len(webAuthEmail) > 0 {
email = webAuthEmail
}
}
var fullname string
if setting.Service.EnableReverseProxyFullName {
fullname = req.Header.Get(setting.ReverseProxyAuthFullName)
}
user := &user_model.User{
Name: username,
Email: email,
FullName: fullname,
}
overwriteDefault := user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(true),
}
// The first user created should be an admin.
if user_model.CountUsers(req.Context(), nil) == 0 {
user.IsAdmin = true
}
if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil {
// FIXME: should I create a system notice?
log.Error("CreateUser: %v", err)
return nil
}
return user
}