12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292 |
- // Copyright 2019 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package auth
-
- import (
- go_context "context"
- "encoding/base64"
- "errors"
- "fmt"
- "html"
- "html/template"
- "io"
- "net/http"
- "net/url"
- "sort"
- "strings"
-
- "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"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- auth_service "code.gitea.io/gitea/services/auth"
- source_service "code.gitea.io/gitea/services/auth/source"
- "code.gitea.io/gitea/services/auth/source/oauth2"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/externalaccount"
- "code.gitea.io/gitea/services/forms"
- user_service "code.gitea.io/gitea/services/user"
-
- "gitea.com/go-chi/binding"
- "github.com/golang-jwt/jwt/v5"
- "github.com/markbates/goth"
- "github.com/markbates/goth/gothic"
- go_oauth2 "golang.org/x/oauth2"
- )
-
- 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
- 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
- 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
- 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
- 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
- }
-
- // 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
- 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"`
- }
-
- 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",
- }
- }
- }
- // generate access token to access the API
- expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
- accessToken := &oauth2.Token{
- GrantID: grant.ID,
- Type: oauth2.TypeAccessToken,
- RegisteredClaims: jwt.RegisteredClaims{
- ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
- },
- }
- signedAccessToken, err := accessToken.SignToken(serverKey)
- 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()
- refreshToken := &oauth2.Token{
- GrantID: grant.ID,
- Counter: grant.Counter,
- Type: oauth2.TypeRefreshToken,
- RegisteredClaims: jwt.RegisteredClaims{
- ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
- },
- }
- signedRefreshToken, err := refreshToken.SignToken(serverKey)
- 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",
- }
- }
-
- 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()
- 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",
- }
- }
- }
-
- return &AccessTokenResponse{
- AccessToken: signedAccessToken,
- TokenType: TokenTypeBearer,
- ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
- RefreshToken: signedRefreshToken,
- IDToken: signedIDToken,
- }, 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=""`)
- 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,
- 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=""`)
- 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)
- }
-
- // AuthorizeOAuth manages authorize requests
- func AuthorizeOAuth(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.AuthorizationForm)
- errs := binding.Errors{}
- errs = form.Validate(ctx.Req, errs)
- if len(errs) > 0 {
- errstring := ""
- for _, e := range errs {
- errstring += e.Error() + "\n"
- }
- ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
- return
- }
-
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- if auth.IsErrOauthClientIDInvalid(err) {
- 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
- }
- }
-
- 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)
- }
- case "":
- // "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
- }
- default:
- // "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
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeInvalidRequest,
- ErrorDescription: "unsupported code challenge method",
- State: form.State,
- }, form.RedirectURI)
- return
- }
-
- grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
- 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)
- 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())
- 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>")
- // TODO document SESSION <=> FORM
- 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)
- }
-
- // GrantApplicationOAuth manages the post request submitted when a user grants access to an application
- func GrantApplicationOAuth(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.GrantApplicationForm)
- 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)
- return
- }
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- ctx.ServerError("GetOAuth2ApplicationByClientID", err)
- return
- }
- 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
- }
- if len(form.Nonce) > 0 {
- err := grant.SetNonce(ctx, form.Nonce)
- if err != nil {
- log.Error("Unable to update nonce: %v", err)
- }
- }
-
- 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)
- 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
- }
- ctx.Redirect(redirect.String(), http.StatusSeeOther)
- }
-
- // 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)
- }
- }
-
- // AccessTokenOAuth manages all access token requests by the client
- 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
- }
- }
-
- switch form.GrantType {
- case "refresh_token":
- handleRefreshToken(ctx, form, serverKey, clientKey)
- case "authorization_code":
- handleAuthorizationCode(ctx, form, serverKey, clientKey)
- 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)
- if err != nil {
- handleAccessTokenError(ctx, AccessTokenError{
- ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "unable to parse refresh token",
- })
- return
- }
- // get grant before increasing counter
- grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
- 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) {
- 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)
- if tokenErr != nil {
- handleAccessTokenError(ctx, *tokenErr)
- return
- }
- ctx.JSON(http.StatusOK, accessToken)
- }
-
- func handleAuthorizationCode(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: '%s'", form.ClientID),
- })
- return
- }
- if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
- errorDescription := "invalid client secret"
- if form.ClientSecret == "" {
- errorDescription = "invalid empty client secret"
- }
- handleAccessTokenError(ctx, AccessTokenError{
- ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: errorDescription,
- })
- return
- }
- if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
- handleAccessTokenError(ctx, AccessTokenError{
- ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "unexpected redirect URI",
- })
- return
- }
- authorizationCode, err := auth.GetOAuth2AuthorizationByCode(ctx, form.Code)
- 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",
- })
- 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 {
- handleAccessTokenError(ctx, AccessTokenError{
- ErrorCode: AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "cannot proceed your request",
- })
- }
- resp, tokenErr := newAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
- if tokenErr != nil {
- handleAccessTokenError(ctx, *tokenErr)
- return
- }
- // send successful response
- ctx.JSON(http.StatusOK, resp)
- }
-
- func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
- ctx.JSON(http.StatusBadRequest, acErr)
- }
-
- func handleServerError(ctx *context.Context, state, redirectURI string) {
- 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)
- 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)
- }
-
- // SignInOAuth handles the OAuth2 login buttons
- func SignInOAuth(ctx *context.Context) {
- provider := ctx.Params(":provider")
-
- authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
- if err != nil {
- ctx.ServerError("SignIn", err)
- return
- }
-
- redirectTo := ctx.FormString("redirect_to")
- if len(redirectTo) > 0 {
- middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
- }
-
- // 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)
- if err == nil && user != nil {
- // we got the user without going through the whole OAuth2 authentication flow again
- handleOAuth2SignIn(ctx, authSource, user, gothUser)
- return
- }
-
- if err = authSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
- if strings.Contains(err.Error(), "no provider for ") {
- if err = oauth2.ResetOAuth2(ctx); err != nil {
- ctx.ServerError("SignIn", err)
- return
- }
- if err = authSource.Cfg.(*oauth2.Source).Callout(ctx.Req, ctx.Resp); err != nil {
- 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)
- }
-
- // first look if the provider is still active
- authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
- 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"))
- return
- }
-
- u, gothUser, err := oAuth2UserLoginCallback(ctx, authSource, ctx.Req, ctx.Resp)
- if err != nil {
- if user_model.IsErrUserProhibitLogin(err) {
- uplerr := err.(user_model.ErrUserProhibitLogin)
- 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)
- }
- ctx.ServerError("UserSignIn", err)
- return
- }
-
- if u == nil {
- if ctx.Doer != nil {
- // attach user to the current signed-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 {
- // create new user with details from oauth2 provider
- var missingFields []string
- if gothUser.UserID == "" {
- missingFields = append(missingFields, "sub")
- }
- if gothUser.Email == "" {
- missingFields = append(missingFields, "email")
- }
- uname, err := extractUserNameFromOAuth2(&gothUser)
- if err != nil {
- ctx.ServerError("UserSignIn", err)
- return
- }
- if uname == "" {
- if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
- missingFields = append(missingFields, "nickname")
- } else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
- missingFields = append(missingFields, "preferred_username")
- } // else: "UserID" and "Email" have been handled above separately
- }
- if len(missingFields) > 0 {
- log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return required fields: %s. `+
- `Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) to request all required fields, and the fields shouldn't be empty.`,
- authSource.Name, strings.Join(missingFields, ","))
- // The RawData is the only way to pass the missing fields to the another page at the moment, other ways all have various problems:
- // by session or cookie: difficult to clean or reset; by URL: could be injected with uncontrollable content; by ctx.Flash: the link_account page is a mess ...
- // Since the RawData is for the provider's data, so we need to use our own prefix here to avoid conflict.
- if gothUser.RawData == nil {
- gothUser.RawData = make(map[string]any)
- }
- gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
- showLinkingLogin(ctx, gothUser)
- return
- }
- 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),
- }
-
- source := authSource.Cfg.(*oauth2.Source)
-
- isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
- u.IsAdmin = isAdmin.ValueOrDefault(false)
- u.IsRestricted = isRestricted.ValueOrDefault(false)
-
- if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
- // error already handled
- return
- }
-
- if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
- ctx.ServerError("SyncGroupsToTeams", err)
- return
- }
- } 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] {
- var groups []string
-
- switch rawGroup := claimValue.(type) {
- case []string:
- groups = rawGroup
- case []any:
- for _, group := range rawGroup {
- groups = append(groups, fmt.Sprintf("%s", group))
- }
- default:
- str := fmt.Sprintf("%s", rawGroup)
- groups = strings.Split(str, ",")
- }
- return container.SetOf(groups...)
- }
-
- 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
- }
- }
-
- return nil
- }
-
- func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
- groupClaims, has := gothUser.RawData[source.GroupClaimName]
- if !has {
- return nil
- }
-
- return claimValueToStringSet(groupClaims)
- }
-
- func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
- groups := getClaimedGroups(source, gothUser)
-
- if source.AdminGroup != "" {
- isAdmin = optional.Some(groups.Contains(source.AdminGroup))
- }
- if source.RestrictedGroup != "" {
- isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
- }
-
- return isAdmin, isRestricted
- }
-
- func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
- if err := updateSession(ctx, nil, map[string]any{
- "linkAccountGothUser": gothUser,
- }); err != nil {
- ctx.ServerError("updateSession", err)
- return
- }
- ctx.Redirect(setting.AppSubURL + "/user/link_account")
- }
-
- func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
- 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)
- }
- }
- }
- }
-
- func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
- updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
-
- needs2FA := false
- if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
- _, err := auth.GetTwoFactorByUID(ctx, u.ID)
- 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)
-
- // 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 {
- if err := updateSession(ctx, nil, map[string]any{
- "uid": u.ID,
- "uname": u.Name,
- }); err != nil {
- ctx.ServerError("updateSession", err)
- return
- }
-
- // Clear whatever CSRF cookie has right now, force to generate a new one
- ctx.Csrf.DeleteCookie(ctx)
-
- opts := &user_service.UpdateOptions{
- SetLastLogin: true,
- }
- opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
- if err := user_service.UpdateUser(ctx, u, opts); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
-
- if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
- if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
- ctx.ServerError("SyncGroupsToTeams", err)
- return
- }
- }
-
- // update external user information
- if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
- if !errors.Is(err, util.ErrNotExist) {
- log.Error("UpdateExternalUser failed: %v", err)
- }
- }
-
- if err := resetLocale(ctx, u); err != nil {
- ctx.ServerError("resetLocale", err)
- return
- }
-
- if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToCurrentSite(redirectTo)
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/")
- return
- }
-
- opts := &user_service.UpdateOptions{}
- opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
- if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
- if err := user_service.UpdateUser(ctx, u, opts); err != nil {
- ctx.ServerError("UpdateUser", err)
- return
- }
- }
-
- 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 := 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)
- return
- }
-
- // If WebAuthn is enrolled -> Redirect to WebAuthn instead
- regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
- if err == nil && len(regs) > 0 {
- ctx.Redirect(setting.AppSubURL + "/user/webauthn")
- return
- }
-
- ctx.Redirect(setting.AppSubURL + "/user/two_factor")
- }
-
- // 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) {
- 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 {
- return nil, goth.User{}, err
- }
-
- return nil, goth.User{}, errCallback{
- Code: errorName,
- Description: errorDescription,
- }
- }
-
- // Proceed to authenticate through goth.
- gothUser, err := oauth2Source.Callback(request, response)
- 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)
- }
- return nil, goth.User{}, err
- }
-
- if oauth2Source.RequiredClaimName != "" {
- claimInterface, has := gothUser.RawData[oauth2Source.RequiredClaimName]
- if !has {
- return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
- }
-
- if oauth2Source.RequiredClaimValue != "" {
- groups := claimValueToStringSet(claimInterface)
-
- if !groups.Contains(oauth2Source.RequiredClaimValue) {
- return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
- }
- }
- }
-
- user := &user_model.User{
- LoginName: gothUser.UserID,
- LoginType: auth.OAuth2,
- LoginSource: authSource.ID,
- }
-
- hasUser, err := user_model.GetUser(ctx, user)
- if err != nil {
- return nil, goth.User{}, err
- }
-
- if hasUser {
- return user, gothUser, nil
- }
-
- // search in external linked users
- externalLoginUser := &user_model.ExternalLoginUser{
- ExternalID: gothUser.UserID,
- LoginSourceID: authSource.ID,
- }
- hasUser, err = user_model.GetExternalLogin(request.Context(), externalLoginUser)
- if err != nil {
- return nil, goth.User{}, err
- }
- if hasUser {
- user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
- return user, gothUser, err
- }
-
- // no user found to login
- return nil, gothUser, nil
- }
|