forgejo/routers/web/auth/oauth.go

1375 lines
44 KiB
Go
Raw Normal View History

2019-03-08 19:42:50 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2019-03-08 19:42:50 +03:00
2022-01-02 16:12:35 +03:00
package auth
2019-03-08 19:42:50 +03:00
import (
go_context "context"
"crypto/sha256"
"encoding/base64"
2022-01-02 16:12:35 +03:00
"errors"
2019-03-08 19:42:50 +03:00
"fmt"
"html"
"html/template"
2022-01-02 16:12:35 +03:00
"io"
"net/http"
2019-03-08 19:42:50 +03:00
"net/url"
"sort"
"strings"
2019-03-08 19:42:50 +03:00
2022-01-02 16:12:35 +03:00
"code.gitea.io/gitea/models/auth"
org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
2019-03-08 19:42:50 +03:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
2019-03-08 19:42:50 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
2019-03-08 19:42:50 +03:00
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
"code.gitea.io/gitea/modules/web"
2022-01-02 16:12:35 +03:00
"code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
2022-01-02 16:12:35 +03:00
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
remote_service "code.gitea.io/gitea/services/remote"
2022-01-02 16:12:35 +03:00
user_service "code.gitea.io/gitea/services/user"
2019-06-12 22:41:28 +03:00
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
"gitea.com/go-chi/binding"
"github.com/golang-jwt/jwt/v5"
2022-01-02 16:12:35 +03:00
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
2024-06-10 11:09:35 +03:00
"github.com/markbates/goth/providers/fitbit"
"github.com/markbates/goth/providers/openidConnect"
"github.com/markbates/goth/providers/zoom"
go_oauth2 "golang.org/x/oauth2"
2019-03-08 19:42:50 +03:00
)
const (
tplGrantAccess base.TplName = "user/auth/grant"
tplGrantError base.TplName = "user/auth/grant_error"
)
// TODO move error and responses to SDK or models
// AuthorizeErrorCode represents an error code specified in RFC 6749
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
2019-03-08 19:42:50 +03:00
type AuthorizeErrorCode string
const (
// ErrorCodeInvalidRequest represents the according error in RFC 6749
ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
// ErrorCodeUnauthorizedClient represents the according error in RFC 6749
ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
// ErrorCodeAccessDenied represents the according error in RFC 6749
ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
// ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
// ErrorCodeInvalidScope represents the according error in RFC 6749
ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
// ErrorCodeServerError represents the according error in RFC 6749
ErrorCodeServerError AuthorizeErrorCode = "server_error"
// ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
)
// AuthorizeError represents an error type specified in RFC 6749
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
2019-03-08 19:42:50 +03:00
type AuthorizeError struct {
ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
ErrorDescription string
State string
}
// Error returns the error message
func (err AuthorizeError) Error() string {
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
}
// AccessTokenErrorCode represents an error code specified in RFC 6749
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
2019-03-08 19:42:50 +03:00
type AccessTokenErrorCode string
const (
// AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
// AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidClient = "invalid_client"
// AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidGrant = "invalid_grant"
// AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
// AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
// AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
AccessTokenErrorCodeInvalidScope = "invalid_scope"
)
// AccessTokenError represents an error response specified in RFC 6749
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
2019-03-08 19:42:50 +03:00
type AccessTokenError struct {
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
ErrorDescription string `json:"error_description"`
}
// Error returns the error message
func (err AccessTokenError) Error() string {
return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
}
// errCallback represents a oauth2 callback error
type errCallback struct {
Code string
Description string
}
func (err errCallback) Error() string {
return err.Description
}
2019-03-08 19:42:50 +03:00
// TokenType specifies the kind of token
type TokenType string
const (
// TokenTypeBearer represents a token type specified in RFC 6749
TokenTypeBearer TokenType = "bearer"
// TokenTypeMAC represents a token type specified in RFC 6749
TokenTypeMAC = "mac"
)
// AccessTokenResponse represents a successful access token response
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
2019-03-08 19:42:50 +03:00
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType TokenType `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
2019-03-08 19:42:50 +03:00
}
func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, serverKey, clientKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "cannot increase the grant counter",
}
2019-03-08 19:42:50 +03:00
}
}
// generate access token to access the API
expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
accessToken := &oauth2.Token{
2019-03-08 19:42:50 +03:00
GrantID: grant.ID,
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
Type: oauth2.TypeAccessToken,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
2019-03-08 19:42:50 +03:00
},
}
signedAccessToken, err := accessToken.SignToken(serverKey)
2019-03-08 19:42:50 +03:00
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
// generate refresh token to request an access token after it expired later
refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime()
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
refreshToken := &oauth2.Token{
2019-03-08 19:42:50 +03:00
GrantID: grant.ID,
Counter: grant.Counter,
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
Type: oauth2.TypeRefreshToken,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
2019-03-08 19:42:50 +03:00
},
}
signedRefreshToken, err := refreshToken.SignToken(serverKey)
2019-03-08 19:42:50 +03:00
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
// generate OpenID Connect id_token
signedIDToken := ""
if grant.ScopeContains("openid") {
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot find application",
}
}
user, err := user_model.GetUserByID(ctx, grant.UserID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot find user",
}
}
log.Error("Error loading user: %v", err)
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "server error",
}
}
Refactor: Move login out of models (#16199) `models` does far too much. In particular it handles all `UserSignin`. It shouldn't be responsible for calling LDAP, SMTP or PAM for signing in. Therefore we should move this code out of `models`. This code has to depend on `models` - therefore it belongs in `services`. There is a package in `services` called `auth` and clearly this functionality belongs in there. Plan: - [x] Change `auth.Auth` to `auth.Method` - as they represent methods of authentication. - [x] Move `models.UserSignIn` into `auth` - [x] Move `models.ExternalUserLogin` - [x] Move most of the `LoginVia*` methods to `auth` or subpackages - [x] Move Resynchronize functionality to `auth` - Involved some restructuring of `models/ssh_key.go` to reduce the size of this massive file and simplify its files. - [x] Move the rest of the LDAP functionality in to the ldap subpackage - [x] Re-factor the login sources to express an interfaces `auth.Source`? - I've done this through some smaller interfaces Authenticator and Synchronizable - which would allow us to extend things in future - [x] Now LDAP is out of models - need to think about modules/auth/ldap and I think all of that functionality might just be moveable - [x] Similarly a lot Oauth2 functionality need not be in models too and should be moved to services/auth/source/oauth2 - [x] modules/auth/oauth2/oauth2.go uses xorm... This is naughty - probably need to move this into models. - [x] models/oauth2.go - mostly should be in modules/auth/oauth2 or services/auth/source/oauth2 - [x] More simplifications of login_source.go may need to be done - Allow wiring in of notify registration - *this can now easily be done - but I think we should do it in another PR* - see #16178 - More refactors...? - OpenID should probably become an auth Method but I think that can be left for another PR - Methods should also probably be cleaned up - again another PR I think. - SSPI still needs more refactors.* Rename auth.Auth auth.Method * Restructure ssh_key.go - move functions from models/user.go that relate to ssh_key to ssh_key - split ssh_key.go to try create clearer function domains for allow for future refactors here. Signed-off-by: Andrew Thornton <art27@cantab.net>
2021-07-24 13:16:34 +03:00
idToken := &oauth2.OIDCToken{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
Issuer: setting.AppURL,
Audience: []string{app.ClientID},
Subject: fmt.Sprint(grant.UserID),
},
Nonce: grant.Nonce,
}
if grant.ScopeContains("profile") {
idToken.Name = user.GetDisplayName()
idToken.PreferredUsername = user.Name
idToken.Profile = user.HTMLURL()
Add context cache as a request level cache (#22294) To avoid duplicated load of the same data in an HTTP request, we can set a context cache to do that. i.e. Some pages may load a user from a database with the same id in different areas on the same page. But the code is hidden in two different deep logic. How should we share the user? As a result of this PR, now if both entry functions accept `context.Context` as the first parameter and we just need to refactor `GetUserByID` to reuse the user from the context cache. Then it will not be loaded twice on an HTTP request. But of course, sometimes we would like to reload an object from the database, that's why `RemoveContextData` is also exposed. The core context cache is here. It defines a new context ```go type cacheContext struct { ctx context.Context data map[any]map[any]any lock sync.RWMutex } var cacheContextKey = struct{}{} func WithCacheContext(ctx context.Context) context.Context { return context.WithValue(ctx, cacheContextKey, &cacheContext{ ctx: ctx, data: make(map[any]map[any]any), }) } ``` Then you can use the below 4 methods to read/write/del the data within the same context. ```go func GetContextData(ctx context.Context, tp, key any) any func SetContextData(ctx context.Context, tp, key, value any) func RemoveContextData(ctx context.Context, tp, key any) func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) ``` Then let's take a look at how `system.GetString` implement it. ```go func GetSetting(ctx context.Context, key string) (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) { res, err := GetSettingNoCache(ctx, key) if err != nil { return "", err } return res.SettingValue, nil }) }) } ``` First, it will check if context data include the setting object with the key. If not, it will query from the global cache which may be memory or a Redis cache. If not, it will get the object from the database. In the end, if the object gets from the global cache or database, it will be set into the context cache. An object stored in the context cache will only be destroyed after the context disappeared.
2023-02-15 16:37:34 +03:00
idToken.Picture = user.AvatarLink(ctx)
idToken.Website = user.Website
idToken.Locale = user.Language
idToken.UpdatedAt = user.UpdatedUnix
}
if grant.ScopeContains("email") {
idToken.Email = user.Email
idToken.EmailVerified = user.IsActive
}
if grant.ScopeContains("groups") {
groups, err := getOAuthGroupsForUser(ctx, user)
if err != nil {
log.Error("Error getting groups: %v", err)
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "server error",
}
}
idToken.Groups = groups
}
signedIDToken, err = idToken.SignToken(clientKey)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot sign token",
}
}
}
2019-03-08 19:42:50 +03:00
return &AccessTokenResponse{
AccessToken: signedAccessToken,
TokenType: TokenTypeBearer,
ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
RefreshToken: signedRefreshToken,
IDToken: signedIDToken,
2019-03-08 19:42:50 +03:00
}, nil
}
type userInfoResponse struct {
Sub string `json:"sub"`
Name string `json:"name"`
Username string `json:"preferred_username"`
Email string `json:"email"`
Picture string `json:"picture"`
Groups []string `json:"groups"`
}
// InfoOAuth manages request for userinfo endpoint
func InfoOAuth(ctx *context.Context) {
if ctx.Doer == nil || ctx.Data["AuthedMethod"] != (&auth_service.OAuth2{}).Name() {
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
2021-12-15 09:59:57 +03:00
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
return
}
response := &userInfoResponse{
Sub: fmt.Sprint(ctx.Doer.ID),
Name: ctx.Doer.FullName,
Username: ctx.Doer.Name,
Email: ctx.Doer.Email,
Add context cache as a request level cache (#22294) To avoid duplicated load of the same data in an HTTP request, we can set a context cache to do that. i.e. Some pages may load a user from a database with the same id in different areas on the same page. But the code is hidden in two different deep logic. How should we share the user? As a result of this PR, now if both entry functions accept `context.Context` as the first parameter and we just need to refactor `GetUserByID` to reuse the user from the context cache. Then it will not be loaded twice on an HTTP request. But of course, sometimes we would like to reload an object from the database, that's why `RemoveContextData` is also exposed. The core context cache is here. It defines a new context ```go type cacheContext struct { ctx context.Context data map[any]map[any]any lock sync.RWMutex } var cacheContextKey = struct{}{} func WithCacheContext(ctx context.Context) context.Context { return context.WithValue(ctx, cacheContextKey, &cacheContext{ ctx: ctx, data: make(map[any]map[any]any), }) } ``` Then you can use the below 4 methods to read/write/del the data within the same context. ```go func GetContextData(ctx context.Context, tp, key any) any func SetContextData(ctx context.Context, tp, key, value any) func RemoveContextData(ctx context.Context, tp, key any) func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) ``` Then let's take a look at how `system.GetString` implement it. ```go func GetSetting(ctx context.Context, key string) (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) { res, err := GetSettingNoCache(ctx, key) if err != nil { return "", err } return res.SettingValue, nil }) }) } ``` First, it will check if context data include the setting object with the key. If not, it will query from the global cache which may be memory or a Redis cache. If not, it will get the object from the database. In the end, if the object gets from the global cache or database, it will be set into the context cache. An object stored in the context cache will only be destroyed after the context disappeared.
2023-02-15 16:37:34 +03:00
Picture: ctx.Doer.AvatarLink(ctx),
}
groups, err := getOAuthGroupsForUser(ctx, ctx.Doer)
if err != nil {
ctx.ServerError("Oauth groups for user", err)
return
}
response.Groups = groups
ctx.JSON(http.StatusOK, response)
}
// returns a list of "org" and "org:team" strings,
// that the given user is a part of.
func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User) ([]string, error) {
orgs, err := org_model.GetUserOrgsList(ctx, user)
if err != nil {
return nil, fmt.Errorf("GetUserOrgList: %w", err)
}
var groups []string
for _, org := range orgs {
groups = append(groups, org.Name)
teams, err := org.LoadTeams(ctx)
if err != nil {
return nil, fmt.Errorf("LoadTeams: %w", err)
}
for _, team := range teams {
if team.IsMember(ctx, user.ID) {
groups = append(groups, org.Name+":"+team.LowerName)
}
}
}
return groups, nil
}
// IntrospectOAuth introspects an oauth token
func IntrospectOAuth(ctx *context.Context) {
if ctx.Doer == nil {
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
2021-12-15 09:59:57 +03:00
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
return
}
var response struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
token, err := oauth2.ParseToken(form.Token, oauth2.DefaultSigningKey)
if err == nil {
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
if err == nil && grant != nil {
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
if err == nil && app != nil {
response.Active = true
response.Scope = grant.Scope
response.Issuer = setting.AppURL
response.Audience = []string{app.ClientID}
response.Subject = fmt.Sprint(grant.UserID)
}
}
}
ctx.JSON(http.StatusOK, response)
}
2019-03-08 19:42:50 +03:00
// AuthorizeOAuth manages authorize requests
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
func AuthorizeOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AuthorizationForm)
2019-03-08 19:42:50 +03:00
errs := binding.Errors{}
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
errs = form.Validate(ctx.Req, errs)
2019-06-12 22:41:28 +03:00
if len(errs) > 0 {
errstring := ""
for _, e := range errs {
errstring += e.Error() + "\n"
}
2019-06-13 07:23:45 +03:00
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
2019-06-12 22:41:28 +03:00
return
}
2019-03-08 19:42:50 +03:00
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
2019-03-08 19:42:50 +03:00
if err != nil {
2022-01-02 16:12:35 +03:00
if auth.IsErrOauthClientIDInvalid(err) {
2019-03-08 19:42:50 +03:00
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeUnauthorizedClient,
ErrorDescription: "Client ID not registered",
State: form.State,
}, "")
return
}
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
var user *user_model.User
if app.UID != 0 {
user, err = user_model.GetUserByID(ctx, app.UID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
2019-03-08 19:42:50 +03:00
}
if !app.ContainsRedirectURI(form.RedirectURI) {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeInvalidRequest,
ErrorDescription: "Unregistered Redirect URI",
State: form.State,
}, "")
return
}
if form.ResponseType != "code" {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeUnsupportedResponseType,
ErrorDescription: "Only code response type is supported.",
State: form.State,
}, form.RedirectURI)
return
}
// pkce support
switch form.CodeChallengeMethod {
case "S256":
case "plain":
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "cannot set code challenge method",
State: form.State,
}, form.RedirectURI)
return
}
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "cannot set code challenge",
State: form.State,
}, form.RedirectURI)
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
2019-03-08 19:42:50 +03:00
case "":
Record OAuth client type at registration (#21316) The OAuth spec [defines two types of client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1), confidential and public. Previously Gitea assumed all clients to be confidential. > OAuth defines two client types, based on their ability to authenticate securely with the authorization server (i.e., ability to > maintain the confidentiality of their client credentials): > > confidential > Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with > restricted access to the client credentials), or capable of secure client authentication using other means. > > **public > Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.** > > The client type designation is based on the authorization server's definition of secure authentication and its acceptable exposure levels of client credentials. The authorization server SHOULD NOT make assumptions about the client type. https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 > Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly. Require PKCE for public clients: https://datatracker.ietf.org/doc/html/rfc8252#section-8.1 > Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message Fixes #21299 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-10-24 10:59:24 +03:00
// "Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message"
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.1
if !app.ConfidentialClient {
// "the authorization endpoint MUST return the authorization error response with the "error" value set to "invalid_request""
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeInvalidRequest,
ErrorDescription: "PKCE is required for public clients",
State: form.State,
}, form.RedirectURI)
return
}
2019-03-08 19:42:50 +03:00
default:
Record OAuth client type at registration (#21316) The OAuth spec [defines two types of client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1), confidential and public. Previously Gitea assumed all clients to be confidential. > OAuth defines two client types, based on their ability to authenticate securely with the authorization server (i.e., ability to > maintain the confidentiality of their client credentials): > > confidential > Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with > restricted access to the client credentials), or capable of secure client authentication using other means. > > **public > Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.** > > The client type designation is based on the authorization server's definition of secure authentication and its acceptable exposure levels of client credentials. The authorization server SHOULD NOT make assumptions about the client type. https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 > Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly. Require PKCE for public clients: https://datatracker.ietf.org/doc/html/rfc8252#section-8.1 > Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message Fixes #21299 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-10-24 10:59:24 +03:00
// "If the server supporting PKCE does not support the requested transformation, the authorization endpoint MUST return the authorization error response with "error" value set to "invalid_request"."
// https://www.rfc-editor.org/rfc/rfc7636#section-4.4.1
2019-03-08 19:42:50 +03:00
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeInvalidRequest,
ErrorDescription: "unsupported code challenge method",
State: form.State,
}, form.RedirectURI)
return
}
grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
2019-03-08 19:42:50 +03:00
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
// Redirect if user already granted access and the application is confidential.
// I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2
if app.ConfidentialClient && grant != nil {
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
2019-03-08 19:42:50 +03:00
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
redirect, err := code.GenerateRedirectURI(form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
// Update nonce to reflect the new session
if len(form.Nonce) > 0 {
err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
}
ctx.Redirect(redirect.String())
2019-03-08 19:42:50 +03:00
return
}
// show authorize page to grant access
ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI
ctx.Data["State"] = form.State
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
if user != nil {
ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
} else {
ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
}
ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("<strong>" + html.EscapeString(form.RedirectURI) + "</strong>")
2019-03-08 19:42:50 +03:00
// TODO document SESSION <=> FORM
2019-06-12 22:41:28 +03:00
err = ctx.Session.Set("client_id", app.ClientID)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
err = ctx.Session.Set("redirect_uri", form.RedirectURI)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
err = ctx.Session.Set("state", form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
log.Error(err.Error())
return
}
// Here we're just going to try to release the session early
if err := ctx.Session.Release(); err != nil {
// we'll tolerate errors here as they *should* get saved elsewhere
log.Error("Unable to save changes to the session: %v", err)
}
ctx.HTML(http.StatusOK, tplGrantAccess)
2019-03-08 19:42:50 +03:00
}
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
func GrantApplicationOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.GrantApplicationForm)
2019-03-08 19:42:50 +03:00
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
ctx.Session.Get("redirect_uri") != form.RedirectURI {
ctx.Error(http.StatusBadRequest)
2019-03-08 19:42:50 +03:00
return
}
if !form.Granted {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "the request is denied",
ErrorCode: ErrorCodeAccessDenied,
}, form.RedirectURI)
return
}
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
2019-03-08 19:42:50 +03:00
if err != nil {
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
2019-03-08 19:42:50 +03:00
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
if grant == nil {
grant, err = app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
if err != nil {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "cannot create grant for user",
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
} else if grant.Scope != form.Scope {
2019-03-08 19:42:50 +03:00
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
ErrorDescription: "a grant exists with different scope",
2019-03-08 19:42:50 +03:00
ErrorCode: ErrorCodeServerError,
}, form.RedirectURI)
return
}
if len(form.Nonce) > 0 {
err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
}
2019-03-08 19:42:50 +03:00
var codeChallenge, codeChallengeMethod string
codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, codeChallenge, codeChallengeMethod)
2019-03-08 19:42:50 +03:00
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
}
redirect, err := code.GenerateRedirectURI(form.State)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
2019-04-25 14:30:38 +03:00
return
2019-03-08 19:42:50 +03:00
}
ctx.Redirect(redirect.String(), http.StatusSeeOther)
2019-03-08 19:42:50 +03:00
}
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
ctx.JSONTemplate("user/auth/oidc_wellknown")
}
// OIDCKeys generates the JSON Web Key Set
func OIDCKeys(ctx *context.Context) {
jwk, err := oauth2.DefaultSigningKey.ToJWK()
if err != nil {
log.Error("Error converting signing key to JWK: %v", err)
ctx.Error(http.StatusInternalServerError)
return
}
jwk["use"] = "sig"
jwks := map[string][]map[string]string{
"keys": {
jwk,
},
}
ctx.Resp.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(ctx.Resp)
if err := enc.Encode(jwks); err != nil {
log.Error("Failed to encode representation as json. Error: %v", err)
}
}
2019-03-08 19:42:50 +03:00
// AccessTokenOAuth manages all access token requests by the client
Move macaron to chi (#14293) Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
2021-01-26 18:36:53 +03:00
func AccessTokenOAuth(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
if form.ClientID == "" || form.ClientSecret == "" {
authHeader := ctx.Req.Header.Get("Authorization")
authContent := strings.SplitN(authHeader, " ", 2)
if len(authContent) == 2 && authContent[0] == "Basic" {
payload, err := base64.StdEncoding.DecodeString(authContent[1])
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot parse basic auth header",
})
return
}
pair := strings.SplitN(string(payload), ":", 2)
if len(pair) != 2 {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot parse basic auth header",
})
return
}
if form.ClientID != "" && form.ClientID != pair[0] {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "client_id in request body inconsistent with Authorization header",
})
return
}
form.ClientID = pair[0]
if form.ClientSecret != "" && form.ClientSecret != pair[1] {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "client_secret in request body inconsistent with Authorization header",
})
return
}
form.ClientSecret = pair[1]
}
}
serverKey := oauth2.DefaultSigningKey
clientKey := serverKey
if serverKey.IsSymmetric() {
var err error
clientKey, err = oauth2.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "Error creating signing key",
})
return
}
}
2019-03-08 19:42:50 +03:00
switch form.GrantType {
case "refresh_token":
handleRefreshToken(ctx, form, serverKey, clientKey)
2019-03-08 19:42:50 +03:00
case "authorization_code":
handleAuthorizationCode(ctx, form, serverKey, clientKey)
2019-03-08 19:42:50 +03:00
default:
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnsupportedGrantType,
ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
})
}
}
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidClient,
ErrorDescription: fmt.Sprintf("cannot load client with client id: %q", form.ClientID),
})
return
}
// "The authorization server MUST ... require client authentication for confidential clients"
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
errorDescription := "invalid client secret"
if form.ClientSecret == "" {
errorDescription = "invalid empty client secret"
}
// "invalid_client ... Client authentication failed"
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidClient,
ErrorDescription: errorDescription,
})
return
}
token, err := oauth2.ParseToken(form.RefreshToken, serverKey)
2019-03-08 19:42:50 +03:00
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "unable to parse refresh token",
2019-03-08 19:42:50 +03:00
})
return
}
// get grant before increasing counter
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
2019-03-08 19:42:50 +03:00
if err != nil || grant == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "grant does not exist",
})
return
}
// check if token got already used
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
2019-03-08 19:42:50 +03:00
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "token was already used",
})
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
return
}
accessToken, tokenErr := newAccessTokenResponse(ctx, grant, serverKey, clientKey)
2019-03-08 19:42:50 +03:00
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
}
ctx.JSON(http.StatusOK, accessToken)
2019-03-08 19:42:50 +03:00
}
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
2019-03-08 19:42:50 +03:00
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidClient,
ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
2019-03-08 19:42:50 +03:00
})
return
}
if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
errorDescription := "invalid client secret"
if form.ClientSecret == "" {
errorDescription = "invalid empty client secret"
}
2019-03-08 19:42:50 +03:00
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: errorDescription,
2019-03-08 19:42:50 +03:00
})
return
}
if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "unexpected redirect URI",
2019-03-08 19:42:50 +03:00
})
return
}
authorizationCode, err := auth.GetOAuth2AuthorizationByCode(ctx, form.Code)
2019-03-08 19:42:50 +03:00
if err != nil || authorizationCode == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "client is not authorized",
})
return
}
// check if code verifier authorizes the client, PKCE support
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
ErrorDescription: "failed PKCE code challenge",
2019-03-08 19:42:50 +03:00
})
return
}
// check if granted for this application
if authorizationCode.Grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "invalid grant",
})
return
}
// remove token from database to deny duplicate usage
if err := authorizationCode.Invalidate(ctx); err != nil {
2019-03-08 19:42:50 +03:00
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot proceed your request",
})
}
resp, tokenErr := newAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
2019-03-08 19:42:50 +03:00
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
}
// send successful response
ctx.JSON(http.StatusOK, resp)
2019-03-08 19:42:50 +03:00
}
func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
ctx.JSON(http.StatusBadRequest, acErr)
2019-03-08 19:42:50 +03:00
}
func handleServerError(ctx *context.Context, state, redirectURI string) {
2019-03-08 19:42:50 +03:00
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "A server error occurred",
State: state,
}, redirectURI)
}
func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
if redirectURI == "" {
log.Warn("Authorization failed: %v", authErr.ErrorDescription)
ctx.Data["Error"] = authErr
ctx.HTML(http.StatusBadRequest, tplGrantError)
2019-03-08 19:42:50 +03:00
return
}
redirect, err := url.Parse(redirectURI)
if err != nil {
ctx.ServerError("url.Parse", err)
return
}
q := redirect.Query()
q.Set("error", string(authErr.ErrorCode))
q.Set("error_description", authErr.ErrorDescription)
q.Set("state", authErr.State)
redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String(), http.StatusSeeOther)
2019-03-08 19:42:50 +03:00
}
2022-01-02 16:12:35 +03:00
// SignInOAuth handles the OAuth2 login buttons
func SignInOAuth(ctx *context.Context) {
provider := ctx.Params(":provider")
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
2022-01-02 16:12:35 +03:00
if err != nil {
ctx.ServerError("SignIn", err)
return
}
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
}
2022-01-02 16:12:35 +03:00
// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
user, gothUser, err := oAuth2UserLoginCallback(ctx, authSource, ctx.Req, ctx.Resp)
2022-01-02 16:12:35 +03:00
if err == nil && user != nil {
// we got the user without going through the whole OAuth2 authentication flow again
handleOAuth2SignIn(ctx, authSource, user, gothUser)
return
}
2024-06-10 11:09:35 +03:00
codeChallenge, err := generateCodeChallenge(ctx, provider)
if err != nil {
ctx.ServerError("SignIn", fmt.Errorf("could not generate code_challenge: %w", err))
return
}
if err = authSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp, codeChallenge); err != nil {
2022-01-02 16:12:35 +03:00
if strings.Contains(err.Error(), "no provider for ") {
if err = oauth2.ResetOAuth2(ctx); err != nil {
2022-01-02 16:12:35 +03:00
ctx.ServerError("SignIn", err)
return
}
if err = authSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp, codeChallenge); err != nil {
2022-01-02 16:12:35 +03:00
ctx.ServerError("SignIn", err)
}
return
}
ctx.ServerError("SignIn", err)
}
// redirect is done in oauth2.Auth
}
// SignInOAuthCallback handles the callback from the given provider
func SignInOAuthCallback(ctx *context.Context) {
provider := ctx.Params(":provider")
if ctx.Req.FormValue("error") != "" {
var errorKeyValues []string
for k, vv := range ctx.Req.Form {
for _, v := range vv {
errorKeyValues = append(errorKeyValues, fmt.Sprintf("%s = %s", html.EscapeString(k), html.EscapeString(v)))
}
}
sort.Strings(errorKeyValues)
ctx.Flash.Error(strings.Join(errorKeyValues, "<br>"), true)
}
2022-01-02 16:12:35 +03:00
// first look if the provider is still active
authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
2022-01-02 16:12:35 +03:00
if err != nil {
ctx.ServerError("SignIn", err)
return
}
if authSource == nil {
ctx.ServerError("SignIn", errors.New("no valid provider found, check configured callback url in provider"))
2022-01-02 16:12:35 +03:00
return
}
u, gothUser, err := oAuth2UserLoginCallback(ctx, authSource, ctx.Req, ctx.Resp)
2022-01-02 16:12:35 +03:00
if err != nil {
if user_model.IsErrUserProhibitLogin(err) {
uplerr := err.(user_model.ErrUserProhibitLogin)
2022-01-02 16:12:35 +03:00
log.Info("Failed authentication attempt for %s from %s: %v", uplerr.Name, ctx.RemoteAddr(), err)
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
return
}
if callbackErr, ok := err.(errCallback); ok {
log.Info("Failed OAuth callback: (%v) %v", callbackErr.Code, callbackErr.Description)
switch callbackErr.Code {
case "access_denied":
ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.access_denied"))
case "temporarily_unavailable":
ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.temporarily_unavailable"))
default:
ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error"))
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
if err, ok := err.(*go_oauth2.RetrieveError); ok {
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
}
2022-01-02 16:12:35 +03:00
ctx.ServerError("UserSignIn", err)
return
}
if u == nil {
if ctx.Doer != nil {
// attach user to already logged in user
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
if err != nil {
ctx.ServerError("UserLinkAccount", err)
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
return
} else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
2022-01-02 16:12:35 +03:00
// create new user with details from oauth2 provider
if gothUser.UserID == "" {
log.Error("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
}
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
ctx.ServerError("CreateUser", err)
return
2022-01-02 16:12:35 +03:00
}
var missingFields []string
2022-01-02 16:12:35 +03:00
if gothUser.Email == "" {
missingFields = append(missingFields, "email")
}
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
missingFields = append(missingFields, "nickname")
}
if len(missingFields) > 0 {
// we don't have enough information to create an account automatically,
// so we prompt the user for the remaining bits
log.Trace("OAuth2 Provider %s returned empty or missing fields: %s, prompting the user for them", authSource.Name, missingFields)
showLinkingLogin(ctx, gothUser)
2022-01-02 16:12:35 +03:00
return
}
uname, err := getUserName(&gothUser)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
2022-01-02 16:12:35 +03:00
u = &user_model.User{
Name: uname,
FullName: gothUser.Name,
Email: gothUser.Email,
LoginType: auth.OAuth2,
LoginSource: authSource.ID,
LoginName: gothUser.UserID,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
2022-01-02 16:12:35 +03:00
}
source := authSource.Cfg.(*oauth2.Source)
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
u.IsAdmin = isAdmin.ValueOrDefault(false)
u.IsRestricted = isRestricted.ValueOrDefault(false)
2022-01-02 16:12:35 +03:00
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
2022-01-02 16:12:35 +03:00
// error already handled
return
}
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
2022-01-02 16:12:35 +03:00
} else {
// no existing user is found, request attach or new account
showLinkingLogin(ctx, gothUser)
return
}
}
handleOAuth2SignIn(ctx, authSource, u, gothUser)
}
func claimValueToStringSet(claimValue any) container.Set[string] {
2022-01-02 16:12:35 +03:00
var groups []string
switch rawGroup := claimValue.(type) {
case []string:
groups = rawGroup
case []any:
for _, group := range rawGroup {
groups = append(groups, fmt.Sprintf("%s", group))
}
2022-01-02 16:12:35 +03:00
default:
str := fmt.Sprintf("%s", rawGroup)
groups = strings.Split(str, ",")
}
return container.SetOf(groups...)
2022-01-02 16:12:35 +03:00
}
func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
if err != nil {
return err
}
groups := getClaimedGroups(source, gothUser)
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
return err
}
2022-01-02 16:12:35 +03:00
}
return nil
}
func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
2022-01-02 16:12:35 +03:00
groupClaims, has := gothUser.RawData[source.GroupClaimName]
if !has {
return nil
2022-01-02 16:12:35 +03:00
}
return claimValueToStringSet(groupClaims)
}
func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
2022-01-02 16:12:35 +03:00
if source.AdminGroup != "" {
isAdmin = optional.Some(groups.Contains(source.AdminGroup))
2022-01-02 16:12:35 +03:00
}
if source.RestrictedGroup != "" {
isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
2022-01-02 16:12:35 +03:00
}
return isAdmin, isRestricted
2022-01-02 16:12:35 +03:00
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
if err := updateSession(ctx, nil, map[string]any{
"linkAccountGothUser": gothUser,
}); err != nil {
ctx.ServerError("updateSession", err)
2022-01-02 16:12:35 +03:00
return
}
ctx.Redirect(setting.AppSubURL + "/user/link_account")
}
func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
2022-01-02 16:12:35 +03:00
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
resp, err := http.Get(url)
if err == nil {
defer func() {
_ = resp.Body.Close()
}()
}
// ignore any error
if err == nil && resp.StatusCode == http.StatusOK {
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
_ = user_service.UploadAvatar(ctx, u, data)
2022-01-02 16:12:35 +03:00
}
}
}
}
func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
2022-01-02 16:12:35 +03:00
needs2FA := false
if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
2022-01-02 16:12:35 +03:00
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err)
return
}
needs2FA = err == nil
}
oauth2Source := source.Cfg.(*oauth2.Source)
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
if err != nil {
ctx.ServerError("UnmarshalGroupTeamMapping", err)
return
}
groups := getClaimedGroups(oauth2Source, &gothUser)
allow synchronizing user status from OAuth2 login providers (#31572) This leverages the existing `sync_external_users` cron job to synchronize the `IsActive` flag on users who use an OAuth2 provider set to synchronize. This synchronization is done by checking for expired access tokens, and using the stored refresh token to request a new access token. If the response back from the OAuth2 provider is the `invalid_grant` error code, the user is marked as inactive. However, the user is able to reactivate their account by logging in the web browser through their OAuth2 flow. Also changed to support this is that a linked `ExternalLoginUser` is always created upon a login or signup via OAuth2. Ideally, we would also refresh permissions from the configured OAuth provider (e.g., admin, restricted and group mappings) to match the implementation of LDAP. However, the OAuth library used for this `goth`, doesn't seem to support issuing a session via refresh tokens. The interface provides a [`RefreshToken` method](https://github.com/markbates/goth/blob/master/provider.go#L20), but the returned `oauth.Token` doesn't implement the `goth.Session` we would need to call `FetchUser`. Due to specific implementations, we would need to build a compatibility function for every provider, since they cast to concrete types (e.g. [Azure](https://github.com/markbates/goth/blob/master/providers/azureadv2/azureadv2.go#L132)) --------- Co-authored-by: Kyle D <kdumontnu@gmail.com> (cherry picked from commit 416c36f3034e228a27258b5a8a15eec4e5e426ba) Conflicts: - tests/integration/auth_ldap_test.go Trivial conflict resolved by manually applying the change. - routers/web/auth/oauth.go Technically not a conflict, but the original PR removed the modules/util import, which in our version, is still in use. Added it back.
2024-07-16 21:33:16 +03:00
opts := &user_service.UpdateOptions{}
// Reactivate user if they are deactivated
if !u.IsActive {
opts.IsActive = optional.Some(true)
}
// Update GroupClaims
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
ctx.ServerError("SyncGroupsToTeams", err)
return
}
}
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
ctx.ServerError("EnsureLinkExternalToUser", err)
return
}
2022-01-02 16:12:35 +03:00
// If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA {
allow synchronizing user status from OAuth2 login providers (#31572) This leverages the existing `sync_external_users` cron job to synchronize the `IsActive` flag on users who use an OAuth2 provider set to synchronize. This synchronization is done by checking for expired access tokens, and using the stored refresh token to request a new access token. If the response back from the OAuth2 provider is the `invalid_grant` error code, the user is marked as inactive. However, the user is able to reactivate their account by logging in the web browser through their OAuth2 flow. Also changed to support this is that a linked `ExternalLoginUser` is always created upon a login or signup via OAuth2. Ideally, we would also refresh permissions from the configured OAuth provider (e.g., admin, restricted and group mappings) to match the implementation of LDAP. However, the OAuth library used for this `goth`, doesn't seem to support issuing a session via refresh tokens. The interface provides a [`RefreshToken` method](https://github.com/markbates/goth/blob/master/provider.go#L20), but the returned `oauth.Token` doesn't implement the `goth.Session` we would need to call `FetchUser`. Due to specific implementations, we would need to build a compatibility function for every provider, since they cast to concrete types (e.g. [Azure](https://github.com/markbates/goth/blob/master/providers/azureadv2/azureadv2.go#L132)) --------- Co-authored-by: Kyle D <kdumontnu@gmail.com> (cherry picked from commit 416c36f3034e228a27258b5a8a15eec4e5e426ba) Conflicts: - tests/integration/auth_ldap_test.go Trivial conflict resolved by manually applying the change. - routers/web/auth/oauth.go Technically not a conflict, but the original PR removed the modules/util import, which in our version, is still in use. Added it back.
2024-07-16 21:33:16 +03:00
// Register last login
opts.SetLastLogin = true
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
if err := updateSession(ctx, nil, map[string]any{
[SECURITY] Rework long-term authentication - This is a 'front-port' of the already existing patch on v1.21 and v1.20, but applied on top of what Gitea has done to rework the LTA mechanism. Forgejo will stick with the reworked mechanism by the Forgejo Security team for the time being. The removal of legacy code (AES-GCM) has been left out. - The current architecture is inherently insecure, because you can construct the 'secret' cookie value with values that are available in the database. Thus provides zero protection when a database is dumped/leaked. - This patch implements a new architecture that's inspired from: [Paragonie Initiative](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies). - Integration testing is added to ensure the new mechanism works. - Removes a setting, because it's not used anymore. (cherry picked from commit e3d6622a63da9c33eed1e3d102cf28a92ff653d6) (cherry picked from commit fef1a6dac5e25579e42d40209c4cfc06879948b9) (cherry picked from commit b0c5165145fa52f2f7bbec1f50b308bdf1d20ef3) (cherry picked from commit 7ad51b9f8d0647eecacd258f6ee26155da3872e1) (cherry picked from commit 64f053f3834e764112cde26bb0d16c5e88d6b2af) (cherry picked from commit f5e78e4c204ce50b800645d614218b6b6096eecb) Conflicts: services/auth/auth_token_test.go https://codeberg.org/forgejo/forgejo/pulls/2069 (cherry picked from commit f69fc23d4bbadf388c7857040ee0774b824e418e) (cherry picked from commit d955ab3ab02cbb7f1245a8cddec426d64d3ac500) (cherry picked from commit 9220088f902a25c4690bcabf5a40a8d02e784182) (cherry picked from commit c73ac636962c41c71814c273510146f0533264ab) (cherry picked from commit 747a176048ea93085b406429db0e25bb21912eda) Conflicts: models/user/user.go routers/web/user/setting/account.go https://codeberg.org/forgejo/forgejo/pulls/2295
2023-11-22 19:26:21 +03:00
"uid": u.ID,
}); err != nil {
ctx.ServerError("updateSession", err)
2022-01-02 16:12:35 +03:00
return
}
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
2022-01-02 16:12:35 +03:00
if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err)
return
}
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
2022-01-02 16:12:35 +03:00
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToFirst(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
return
}
allow synchronizing user status from OAuth2 login providers (#31572) This leverages the existing `sync_external_users` cron job to synchronize the `IsActive` flag on users who use an OAuth2 provider set to synchronize. This synchronization is done by checking for expired access tokens, and using the stored refresh token to request a new access token. If the response back from the OAuth2 provider is the `invalid_grant` error code, the user is marked as inactive. However, the user is able to reactivate their account by logging in the web browser through their OAuth2 flow. Also changed to support this is that a linked `ExternalLoginUser` is always created upon a login or signup via OAuth2. Ideally, we would also refresh permissions from the configured OAuth provider (e.g., admin, restricted and group mappings) to match the implementation of LDAP. However, the OAuth library used for this `goth`, doesn't seem to support issuing a session via refresh tokens. The interface provides a [`RefreshToken` method](https://github.com/markbates/goth/blob/master/provider.go#L20), but the returned `oauth.Token` doesn't implement the `goth.Session` we would need to call `FetchUser`. Due to specific implementations, we would need to build a compatibility function for every provider, since they cast to concrete types (e.g. [Azure](https://github.com/markbates/goth/blob/master/providers/azureadv2/azureadv2.go#L132)) --------- Co-authored-by: Kyle D <kdumontnu@gmail.com> (cherry picked from commit 416c36f3034e228a27258b5a8a15eec4e5e426ba) Conflicts: - tests/integration/auth_ldap_test.go Trivial conflict resolved by manually applying the change. - routers/web/auth/oauth.go Technically not a conflict, but the original PR removed the modules/util import, which in our version, is still in use. Added it back.
2024-07-16 21:33:16 +03:00
if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser", err)
2022-01-02 16:12:35 +03:00
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": false,
}); err != nil {
ctx.ServerError("updateSession", err)
2022-01-02 16:12:35 +03:00
return
}
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
2022-01-02 16:12:35 +03:00
if err == nil && len(regs) > 0 {
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
2022-01-02 16:12:35 +03:00
return
}
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
}
// generateCodeChallenge stores a code verifier in the session and returns a S256 code challenge for PKCE
2024-06-10 11:09:35 +03:00
func generateCodeChallenge(ctx *context.Context, provider string) (codeChallenge string, err error) {
// the `code_verifier` is only forwarded by specific providers
// https://codeberg.org/forgejo/forgejo/issues/4033
p, ok := goth.GetProviders()[provider]
if !ok {
return "", nil
}
switch p.(type) {
default:
return "", nil
case *openidConnect.Provider, *fitbit.Provider, *zoom.Provider:
// those providers forward the `code_verifier`
// a code_challenge can be generated
}
codeVerifier, err := util.CryptoRandomString(43) // 256/log2(62) = 256 bits of entropy (each char having log2(62) of randomness)
if err != nil {
return "", err
}
if err = ctx.Session.Set("CodeVerifier", codeVerifier); err != nil {
return "", err
}
return encodeCodeChallenge(codeVerifier)
}
func encodeCodeChallenge(codeVerifier string) (string, error) {
hasher := sha256.New()
_, err := io.WriteString(hasher, codeVerifier)
codeChallenge := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return codeChallenge, err
}
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
// login the user
func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
gothUser, err := oAuth2FetchUser(ctx, authSource, request, response)
if err != nil {
return nil, goth.User{}, err
}
if _, _, err := remote_service.MaybePromoteRemoteUser(ctx, authSource, gothUser.UserID, gothUser.Email); err != nil {
return nil, goth.User{}, err
}
u, err := oAuth2GothUserToUser(request.Context(), authSource, gothUser)
return u, gothUser, err
}
func oAuth2FetchUser(ctx *context.Context, authSource *auth.Source, request *http.Request, response http.ResponseWriter) (goth.User, error) {
2022-01-02 16:12:35 +03:00
oauth2Source := authSource.Cfg.(*oauth2.Source)
// Make sure that the response is not an error response.
errorName := request.FormValue("error")
if len(errorName) > 0 {
errorDescription := request.FormValue("error_description")
// Delete the goth session
err := gothic.Logout(response, request)
if err != nil {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return goth.User{}, err
}
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return goth.User{}, errCallback{
Code: errorName,
Description: errorDescription,
}
}
// Proceed to authenticate through goth.
codeVerifier, _ := ctx.Session.Get("CodeVerifier").(string)
_ = ctx.Session.Delete("CodeVerifier")
gothUser, err := oauth2Source.Callback(request, response, codeVerifier)
2022-01-02 16:12:35 +03:00
if err != nil {
if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") {
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
}
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return goth.User{}, err
2022-01-02 16:12:35 +03:00
}
if oauth2Source.RequiredClaimName != "" {
claimInterface, has := gothUser.RawData[oauth2Source.RequiredClaimName]
if !has {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
2022-01-02 16:12:35 +03:00
}
if oauth2Source.RequiredClaimValue != "" {
groups := claimValueToStringSet(claimInterface)
if !groups.Contains(oauth2Source.RequiredClaimValue) {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
2022-01-02 16:12:35 +03:00
}
}
}
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return gothUser, nil
}
func oAuth2GothUserToUser(ctx go_context.Context, authSource *auth.Source, gothUser goth.User) (*user_model.User, error) {
2022-01-02 16:12:35 +03:00
user := &user_model.User{
LoginName: gothUser.UserID,
LoginType: auth.OAuth2,
LoginSource: authSource.ID,
}
hasUser, err := user_model.GetUser(ctx, user)
2022-01-02 16:12:35 +03:00
if err != nil {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return nil, err
2022-01-02 16:12:35 +03:00
}
if hasUser {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return user, nil
2022-01-02 16:12:35 +03:00
}
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
log.Debug("no user found for LoginName %v, LoginSource %v, LoginType %v", user.LoginName, user.LoginSource, user.LoginType)
2022-01-02 16:12:35 +03:00
// search in external linked users
externalLoginUser := &user_model.ExternalLoginUser{
ExternalID: gothUser.UserID,
LoginSourceID: authSource.ID,
}
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
hasUser, err = user_model.GetExternalLogin(ctx, externalLoginUser)
2022-01-02 16:12:35 +03:00
if err != nil {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return nil, err
2022-01-02 16:12:35 +03:00
}
if hasUser {
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
user, err = user_model.GetUserByID(ctx, externalLoginUser.UserID)
return user, err
2022-01-02 16:12:35 +03:00
}
// no user found to login
Implement remote user login source and promotion to regular user A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
2024-02-25 13:32:59 +03:00
return nil, nil
2022-01-02 16:12:35 +03:00
}