diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2024-10-02 08:03:19 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-02 08:03:19 +0800 |
commit | 3a4a1bffbebd8a6f024a7fc4849cebbd7f0274d4 (patch) | |
tree | e4f37c1b95646e3de8eca4b37a7caa6779c2ceb1 /routers/web/auth/oauth.go | |
parent | 70b7df0e5e5cce5a3561fa5a6a8abd4ebc902e68 (diff) | |
download | gitea-3a4a1bffbebd8a6f024a7fc4849cebbd7f0274d4.tar.gz gitea-3a4a1bffbebd8a6f024a7fc4849cebbd7f0274d4.zip |
Make oauth2 code clear. Move oauth2 provider code to their own packages/files (#32148)
Fix #30266
Replace #31533
Diffstat (limited to 'routers/web/auth/oauth.go')
-rw-r--r-- | routers/web/auth/oauth.go | 844 |
1 files changed, 0 insertions, 844 deletions
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c61a0a6240..ccbb3bebf1 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -4,878 +4,34 @@ package auth import ( - go_context "context" "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/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 -} - -func parseBasicAuth(ctx *context.Context) (username, password string, err error) { - authHeader := ctx.Req.Header.Get("Authorization") - if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") { - return base.BasicAuthDecode(authData) - } - return "", "", errors.New("invalid basic authentication") -} - -// IntrospectOAuth introspects an oauth token -func IntrospectOAuth(ctx *context.Context) { - clientIDValid := false - if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil { - app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID) - if err != nil && !auth.IsErrOauthClientIDInvalid(err) { - // this is likely a database error; log it and respond without details - log.Error("Error retrieving client_id: %v", err) - ctx.Error(http.StatusInternalServerError) - return - } - clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret)) - } - if !clientIDValid { - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm=""`) - ctx.PlainText(http.StatusUnauthorized, "no valid authorization") - return - } - - var response struct { - Active bool `json:"active"` - Scope string `json:"scope,omitempty"` - Username string `json:"username,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) - } - if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil { - response.Username = user.Name - } - } - } - - 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 or trusted otherwise - // I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2 - if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && 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 - } - - 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) - if err != nil { - ctx.ServerError("GetOAuth2ApplicationByClientID", err) - return - } - grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID) - 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 { - handleAuthorizeError(ctx, AuthorizeError{ - State: form.State, - ErrorDescription: "a grant exists with different scope", - 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") - if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") { - clientID, clientSecret, err := base.BasicAuthDecode(authData) - if err != nil { - handleAccessTokenError(ctx, AccessTokenError{ - ErrorCode: AccessTokenErrorCodeInvalidRequest, - ErrorDescription: "cannot parse basic auth header", - }) - return - } - // validate that any fields present in the form match the Basic auth header - if form.ClientID != "" && form.ClientID != clientID { - handleAccessTokenError(ctx, AccessTokenError{ - ErrorCode: AccessTokenErrorCodeInvalidRequest, - ErrorDescription: "client_id in request body inconsistent with Authorization header", - }) - return - } - form.ClientID = clientID - if form.ClientSecret != "" && form.ClientSecret != clientSecret { - handleAccessTokenError(ctx, AccessTokenError{ - ErrorCode: AccessTokenErrorCodeInvalidRequest, - ErrorDescription: "client_secret in request body inconsistent with Authorization header", - }) - return - } - form.ClientSecret = clientSecret - } - } - - 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.PathParam(":provider") |