markup_service "code.gitea.io/gitea/services/markup"
repo_migrations "code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
+ "code.gitea.io/gitea/services/oauth2_provider"
pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
log.Info("ORM engine initialization successful!")
mustInit(system.Init)
mustInitCtx(ctx, oauth2.Init)
-
+ mustInitCtx(ctx, oauth2_provider.Init)
mustInit(release_service.Init)
mustInitCtx(ctx, models.Init)
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")
--- /dev/null
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "errors"
+ "fmt"
+ "html"
+ "html/template"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/web"
+ auth_service "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/oauth2_provider"
+
+ "gitea.com/go-chi/binding"
+ jwt "github.com/golang-jwt/jwt/v5"
+)
+
+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)
+}
+
+// errCallback represents a oauth2 callback error
+type errCallback struct {
+ Code string
+ Description string
+}
+
+func (err errCallback) Error() string {
+ return err.Description
+}
+
+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 := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("Oauth groups for user", err)
+ return
+ }
+ response.Groups = groups
+
+ ctx.JSON(http.StatusOK, response)
+}
+
+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_provider.ParseToken(form.Token, oauth2_provider.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_provider.DefaultSigningKey
+ ctx.JSONTemplate("user/auth/oidc_wellknown")
+}
+
+// OIDCKeys generates the JSON Web Key Set
+func OIDCKeys(ctx *context.Context) {
+ jwk, err := oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "client_id in request body inconsistent with Authorization header",
+ })
+ return
+ }
+ form.ClientID = clientID
+ if form.ClientSecret != "" && form.ClientSecret != clientSecret {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "client_secret in request body inconsistent with Authorization header",
+ })
+ return
+ }
+ form.ClientSecret = clientSecret
+ }
+ }
+
+ serverKey := oauth2_provider.DefaultSigningKey
+ clientKey := serverKey
+ if serverKey.IsSymmetric() {
+ var err error
+ clientKey, err = oauth2_provider.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
+ if err != nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeUnsupportedGrantType,
+ ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
+ })
+ }
+}
+
+func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2_provider.JWTSigningKey) {
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
+ if err != nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidClient,
+ ErrorDescription: errorDescription,
+ })
+ return
+ }
+
+ token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
+ if err != nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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 := oauth2_provider.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_provider.JWTSigningKey) {
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
+ if err != nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.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, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
+ ErrorDescription: errorDescription,
+ })
+ return
+ }
+ if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
+ ErrorDescription: "unexpected redirect URI",
+ })
+ return
+ }
+ authorizationCode, err := auth.GetOAuth2AuthorizationByCode(ctx, form.Code)
+ if err != nil || authorizationCode == nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
+ ErrorDescription: "client is not authorized",
+ })
+ return
+ }
+ // check if code verifier authorizes the client, PKCE support
+ if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
+ ErrorDescription: "failed PKCE code challenge",
+ })
+ return
+ }
+ // check if granted for this application
+ if authorizationCode.Grant.ApplicationID != app.ID {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
+ ErrorDescription: "invalid grant",
+ })
+ return
+ }
+ // remove token from database to deny duplicate usage
+ if err := authorizationCode.Invalidate(ctx); err != nil {
+ handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
+ ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "cannot proceed your request",
+ })
+ }
+ resp, tokenErr := oauth2_provider.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 oauth2_provider.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)
+}
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/oauth2_provider"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
)
-func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2.OIDCToken {
- signingKey, err := oauth2.CreateJWTSigningKey("HS256", make([]byte, 32))
+func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2_provider.OIDCToken {
+ signingKey, err := oauth2_provider.CreateJWTSigningKey("HS256", make([]byte, 32))
assert.NoError(t, err)
assert.NotNil(t, signingKey)
- response, terr := newAccessTokenResponse(db.DefaultContext, grant, signingKey, signingKey)
+ response, terr := oauth2_provider.NewAccessTokenResponse(db.DefaultContext, grant, signingKey, signingKey)
assert.Nil(t, terr)
assert.NotNil(t, response)
- parsedToken, err := jwt.ParseWithClaims(response.IDToken, &oauth2.OIDCToken{}, func(token *jwt.Token) (any, error) {
+ parsedToken, err := jwt.ParseWithClaims(response.IDToken, &oauth2_provider.OIDCToken{}, func(token *jwt.Token) (any, error) {
assert.NotNil(t, token.Method)
assert.Equal(t, signingKey.SigningMethod().Alg(), token.Method.Alg())
return signingKey.VerifyKey(), nil
assert.NoError(t, err)
assert.True(t, parsedToken.Valid)
- oidcToken, ok := parsedToken.Claims.(*oauth2.OIDCToken)
+ oidcToken, ok := parsedToken.Claims.(*oauth2_provider.OIDCToken)
assert.True(t, ok)
assert.NotNil(t, oidcToken)
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/oauth2_provider"
)
// Ensure the struct implements the interface.
if !strings.Contains(accessToken, ".") {
return 0
}
- token, err := oauth2.ParseToken(accessToken, oauth2.DefaultSigningKey)
+ token, err := oauth2_provider.ParseToken(accessToken, oauth2_provider.DefaultSigningKey)
if err != nil {
log.Trace("oauth2.ParseToken: %v", err)
return 0
if grant, err = auth_model.GetOAuth2GrantByID(ctx, token.GrantID); err != nil || grant == nil {
return 0
}
- if token.Type != oauth2.TypeAccessToken {
+ if token.Kind != oauth2_provider.KindAccessToken {
return 0
}
if token.ExpiresAt.Before(time.Now()) || token.IssuedAt.After(time.Now()) {
// Init initializes the oauth source
func Init(ctx context.Context) error {
- if err := InitSigningKey(); err != nil {
- return err
- }
-
// Lock our mutex
gothRWMutex.Lock()
+++ /dev/null
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package oauth2
-
-import (
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/rsa"
- "crypto/x509"
- "encoding/base64"
- "encoding/pem"
- "fmt"
- "math/big"
- "os"
- "path/filepath"
- "strings"
-
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
-
- "github.com/golang-jwt/jwt/v5"
-)
-
-// ErrInvalidAlgorithmType represents an invalid algorithm error.
-type ErrInvalidAlgorithmType struct {
- Algorithm string
-}
-
-func (err ErrInvalidAlgorithmType) Error() string {
- return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm)
-}
-
-// JWTSigningKey represents a algorithm/key pair to sign JWTs
-type JWTSigningKey interface {
- IsSymmetric() bool
- SigningMethod() jwt.SigningMethod
- SignKey() any
- VerifyKey() any
- ToJWK() (map[string]string, error)
- PreProcessToken(*jwt.Token)
-}
-
-type hmacSigningKey struct {
- signingMethod jwt.SigningMethod
- secret []byte
-}
-
-func (key hmacSigningKey) IsSymmetric() bool {
- return true
-}
-
-func (key hmacSigningKey) SigningMethod() jwt.SigningMethod {
- return key.signingMethod
-}
-
-func (key hmacSigningKey) SignKey() any {
- return key.secret
-}
-
-func (key hmacSigningKey) VerifyKey() any {
- return key.secret
-}
-
-func (key hmacSigningKey) ToJWK() (map[string]string, error) {
- return map[string]string{
- "kty": "oct",
- "alg": key.SigningMethod().Alg(),
- }, nil
-}
-
-func (key hmacSigningKey) PreProcessToken(*jwt.Token) {}
-
-type rsaSingingKey struct {
- signingMethod jwt.SigningMethod
- key *rsa.PrivateKey
- id string
-}
-
-func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
- kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey))
- if err != nil {
- return rsaSingingKey{}, err
- }
-
- return rsaSingingKey{
- signingMethod,
- key,
- base64.RawURLEncoding.EncodeToString(kid),
- }, nil
-}
-
-func (key rsaSingingKey) IsSymmetric() bool {
- return false
-}
-
-func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
- return key.signingMethod
-}
-
-func (key rsaSingingKey) SignKey() any {
- return key.key
-}
-
-func (key rsaSingingKey) VerifyKey() any {
- return key.key.Public()
-}
-
-func (key rsaSingingKey) ToJWK() (map[string]string, error) {
- pubKey := key.key.Public().(*rsa.PublicKey)
-
- return map[string]string{
- "kty": "RSA",
- "alg": key.SigningMethod().Alg(),
- "kid": key.id,
- "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
- "n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
- }, nil
-}
-
-func (key rsaSingingKey) PreProcessToken(token *jwt.Token) {
- token.Header["kid"] = key.id
-}
-
-type eddsaSigningKey struct {
- signingMethod jwt.SigningMethod
- key ed25519.PrivateKey
- id string
-}
-
-func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) {
- kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey))
- if err != nil {
- return eddsaSigningKey{}, err
- }
-
- return eddsaSigningKey{
- signingMethod,
- key,
- base64.RawURLEncoding.EncodeToString(kid),
- }, nil
-}
-
-func (key eddsaSigningKey) IsSymmetric() bool {
- return false
-}
-
-func (key eddsaSigningKey) SigningMethod() jwt.SigningMethod {
- return key.signingMethod
-}
-
-func (key eddsaSigningKey) SignKey() any {
- return key.key
-}
-
-func (key eddsaSigningKey) VerifyKey() any {
- return key.key.Public()
-}
-
-func (key eddsaSigningKey) ToJWK() (map[string]string, error) {
- pubKey := key.key.Public().(ed25519.PublicKey)
-
- return map[string]string{
- "alg": key.SigningMethod().Alg(),
- "kid": key.id,
- "kty": "OKP",
- "crv": "Ed25519",
- "x": base64.RawURLEncoding.EncodeToString(pubKey),
- }, nil
-}
-
-func (key eddsaSigningKey) PreProcessToken(token *jwt.Token) {
- token.Header["kid"] = key.id
-}
-
-type ecdsaSingingKey struct {
- signingMethod jwt.SigningMethod
- key *ecdsa.PrivateKey
- id string
-}
-
-func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
- kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey))
- if err != nil {
- return ecdsaSingingKey{}, err
- }
-
- return ecdsaSingingKey{
- signingMethod,
- key,
- base64.RawURLEncoding.EncodeToString(kid),
- }, nil
-}
-
-func (key ecdsaSingingKey) IsSymmetric() bool {
- return false
-}
-
-func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
- return key.signingMethod
-}
-
-func (key ecdsaSingingKey) SignKey() any {
- return key.key
-}
-
-func (key ecdsaSingingKey) VerifyKey() any {
- return key.key.Public()
-}
-
-func (key ecdsaSingingKey) ToJWK() (map[string]string, error) {
- pubKey := key.key.Public().(*ecdsa.PublicKey)
-
- return map[string]string{
- "kty": "EC",
- "alg": key.SigningMethod().Alg(),
- "kid": key.id,
- "crv": pubKey.Params().Name,
- "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()),
- "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()),
- }, nil
-}
-
-func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
- token.Header["kid"] = key.id
-}
-
-// CreateJWTSigningKey creates a signing key from an algorithm / key pair.
-func CreateJWTSigningKey(algorithm string, key any) (JWTSigningKey, error) {
- var signingMethod jwt.SigningMethod
- switch algorithm {
- case "HS256":
- signingMethod = jwt.SigningMethodHS256
- case "HS384":
- signingMethod = jwt.SigningMethodHS384
- case "HS512":
- signingMethod = jwt.SigningMethodHS512
-
- case "RS256":
- signingMethod = jwt.SigningMethodRS256
- case "RS384":
- signingMethod = jwt.SigningMethodRS384
- case "RS512":
- signingMethod = jwt.SigningMethodRS512
-
- case "ES256":
- signingMethod = jwt.SigningMethodES256
- case "ES384":
- signingMethod = jwt.SigningMethodES384
- case "ES512":
- signingMethod = jwt.SigningMethodES512
- case "EdDSA":
- signingMethod = jwt.SigningMethodEdDSA
- default:
- return nil, ErrInvalidAlgorithmType{algorithm}
- }
-
- switch signingMethod.(type) {
- case *jwt.SigningMethodEd25519:
- privateKey, ok := key.(ed25519.PrivateKey)
- if !ok {
- return nil, jwt.ErrInvalidKeyType
- }
- return newEdDSASingingKey(signingMethod, privateKey)
- case *jwt.SigningMethodECDSA:
- privateKey, ok := key.(*ecdsa.PrivateKey)
- if !ok {
- return nil, jwt.ErrInvalidKeyType
- }
- return newECDSASingingKey(signingMethod, privateKey)
- case *jwt.SigningMethodRSA:
- privateKey, ok := key.(*rsa.PrivateKey)
- if !ok {
- return nil, jwt.ErrInvalidKeyType
- }
- return newRSASingingKey(signingMethod, privateKey)
- default:
- secret, ok := key.([]byte)
- if !ok {
- return nil, jwt.ErrInvalidKeyType
- }
- return hmacSigningKey{signingMethod, secret}, nil
- }
-}
-
-// DefaultSigningKey is the default signing key for JWTs.
-var DefaultSigningKey JWTSigningKey
-
-// InitSigningKey creates the default signing key from settings or creates a random key.
-func InitSigningKey() error {
- var err error
- var key any
-
- switch setting.OAuth2.JWTSigningAlgorithm {
- case "HS256":
- fallthrough
- case "HS384":
- fallthrough
- case "HS512":
- key = setting.GetGeneralTokenSigningSecret()
- case "RS256":
- fallthrough
- case "RS384":
- fallthrough
- case "RS512":
- fallthrough
- case "ES256":
- fallthrough
- case "ES384":
- fallthrough
- case "ES512":
- fallthrough
- case "EdDSA":
- key, err = loadOrCreateAsymmetricKey()
- default:
- return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
- }
-
- if err != nil {
- return fmt.Errorf("Error while loading or creating JWT key: %w", err)
- }
-
- signingKey, err := CreateJWTSigningKey(setting.OAuth2.JWTSigningAlgorithm, key)
- if err != nil {
- return err
- }
-
- DefaultSigningKey = signingKey
-
- return nil
-}
-
-// loadOrCreateAsymmetricKey checks if the configured private key exists.
-// If it does not exist a new random key gets generated and saved on the configured path.
-func loadOrCreateAsymmetricKey() (any, error) {
- keyPath := setting.OAuth2.JWTSigningPrivateKeyFile
-
- isExist, err := util.IsExist(keyPath)
- if err != nil {
- log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err)
- }
- if !isExist {
- err := func() error {
- key, err := func() (any, error) {
- switch {
- case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"):
- return rsa.GenerateKey(rand.Reader, 4096)
- case setting.OAuth2.JWTSigningAlgorithm == "EdDSA":
- _, pk, err := ed25519.GenerateKey(rand.Reader)
- return pk, err
- default:
- return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- }
- }()
- if err != nil {
- return err
- }
-
- bytes, err := x509.MarshalPKCS8PrivateKey(key)
- if err != nil {
- return err
- }
-
- privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes}
-
- if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
- return err
- }
-
- f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
- if err != nil {
- return err
- }
- defer func() {
- if err = f.Close(); err != nil {
- log.Error("Close: %v", err)
- }
- }()
-
- return pem.Encode(f, privateKeyPEM)
- }()
- if err != nil {
- log.Fatal("Error generating private key: %v", err)
- return nil, err
- }
- }
-
- bytes, err := os.ReadFile(keyPath)
- if err != nil {
- return nil, err
- }
-
- block, _ := pem.Decode(bytes)
- if block == nil {
- return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
- } else if block.Type != "PRIVATE KEY" {
- return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath)
- }
-
- return x509.ParsePKCS8PrivateKey(block.Bytes)
-}
+++ /dev/null
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package oauth2
-
-import (
- "fmt"
- "time"
-
- "code.gitea.io/gitea/modules/timeutil"
-
- "github.com/golang-jwt/jwt/v5"
-)
-
-// ___________ __
-// \__ ___/___ | | __ ____ ____
-// | | / _ \| |/ // __ \ / \
-// | |( <_> ) <\ ___/| | \
-// |____| \____/|__|_ \\___ >___| /
-// \/ \/ \/
-
-// Token represents an Oauth grant
-
-// TokenType represents the type of token for an oauth application
-type TokenType int
-
-const (
- // TypeAccessToken is a token with short lifetime to access the api
- TypeAccessToken TokenType = 0
- // TypeRefreshToken is token with long lifetime to refresh access tokens obtained by the client
- TypeRefreshToken = iota
-)
-
-// Token represents a JWT token used to authenticate a client
-type Token struct {
- GrantID int64 `json:"gnt"`
- Type TokenType `json:"tt"`
- Counter int64 `json:"cnt,omitempty"`
- jwt.RegisteredClaims
-}
-
-// ParseToken parses a signed jwt string
-func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
- parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (any, error) {
- if token.Method == nil || token.Method.Alg() != signingKey.SigningMethod().Alg() {
- return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
- }
- return signingKey.VerifyKey(), nil
- })
- if err != nil {
- return nil, err
- }
- if !parsedToken.Valid {
- return nil, fmt.Errorf("invalid token")
- }
- var token *Token
- var ok bool
- if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
- return nil, fmt.Errorf("invalid token")
- }
- return token, nil
-}
-
-// SignToken signs the token with the JWT secret
-func (token *Token) SignToken(signingKey JWTSigningKey) (string, error) {
- token.IssuedAt = jwt.NewNumericDate(time.Now())
- jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
- signingKey.PreProcessToken(jwtToken)
- return jwtToken.SignedString(signingKey.SignKey())
-}
-
-// OIDCToken represents an OpenID Connect id_token
-type OIDCToken struct {
- jwt.RegisteredClaims
- Nonce string `json:"nonce,omitempty"`
-
- // Scope profile
- Name string `json:"name,omitempty"`
- PreferredUsername string `json:"preferred_username,omitempty"`
- Profile string `json:"profile,omitempty"`
- Picture string `json:"picture,omitempty"`
- Website string `json:"website,omitempty"`
- Locale string `json:"locale,omitempty"`
- UpdatedAt timeutil.TimeStamp `json:"updated_at,omitempty"`
-
- // Scope email
- Email string `json:"email,omitempty"`
- EmailVerified bool `json:"email_verified,omitempty"`
-
- // Groups are generated by organization and team names
- Groups []string `json:"groups,omitempty"`
-}
-
-// SignToken signs an id_token with the (symmetric) client secret key
-func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
- token.IssuedAt = jwt.NewNumericDate(time.Now())
- jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
- signingKey.PreProcessToken(jwtToken)
- return jwtToken.SignedString(signingKey.SignKey())
-}
--- /dev/null
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package oauth2_provider //nolint
+
+import (
+ "context"
+ "fmt"
+
+ auth "code.gitea.io/gitea/models/auth"
+ org_model "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// 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)
+}
+
+// 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 context.Context, grant *auth.OAuth2Grant, serverKey, clientKey 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 := &Token{
+ GrantID: grant.ID,
+ Kind: KindAccessToken,
+ 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 := &Token{
+ GrantID: grant.ID,
+ Counter: grant.Counter,
+ Kind: KindRefreshToken,
+ 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 := &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
+}
+
+// returns a list of "org" and "org:team" strings,
+// that the given user is a part of.
+func GetOAuthGroupsForUser(ctx 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
+}
--- /dev/null
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package oauth2_provider //nolint
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// Init initializes the oauth source
+func Init(ctx context.Context) error {
+ if !setting.OAuth2.Enabled {
+ return nil
+ }
+
+ return InitSigningKey()
+}
--- /dev/null
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package oauth2_provider //nolint
+
+import (
+ "crypto/ecdsa"
+ "crypto/ed25519"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// ErrInvalidAlgorithmType represents an invalid algorithm error.
+type ErrInvalidAlgorithmType struct {
+ Algorithm string
+}
+
+func (err ErrInvalidAlgorithmType) Error() string {
+ return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm)
+}
+
+// JWTSigningKey represents a algorithm/key pair to sign JWTs
+type JWTSigningKey interface {
+ IsSymmetric() bool
+ SigningMethod() jwt.SigningMethod
+ SignKey() any
+ VerifyKey() any
+ ToJWK() (map[string]string, error)
+ PreProcessToken(*jwt.Token)
+}
+
+type hmacSigningKey struct {
+ signingMethod jwt.SigningMethod
+ secret []byte
+}
+
+func (key hmacSigningKey) IsSymmetric() bool {
+ return true
+}
+
+func (key hmacSigningKey) SigningMethod() jwt.SigningMethod {
+ return key.signingMethod
+}
+
+func (key hmacSigningKey) SignKey() any {
+ return key.secret
+}
+
+func (key hmacSigningKey) VerifyKey() any {
+ return key.secret
+}
+
+func (key hmacSigningKey) ToJWK() (map[string]string, error) {
+ return map[string]string{
+ "kty": "oct",
+ "alg": key.SigningMethod().Alg(),
+ }, nil
+}
+
+func (key hmacSigningKey) PreProcessToken(*jwt.Token) {}
+
+type rsaSingingKey struct {
+ signingMethod jwt.SigningMethod
+ key *rsa.PrivateKey
+ id string
+}
+
+func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
+ kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey))
+ if err != nil {
+ return rsaSingingKey{}, err
+ }
+
+ return rsaSingingKey{
+ signingMethod,
+ key,
+ base64.RawURLEncoding.EncodeToString(kid),
+ }, nil
+}
+
+func (key rsaSingingKey) IsSymmetric() bool {
+ return false
+}
+
+func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
+ return key.signingMethod
+}
+
+func (key rsaSingingKey) SignKey() any {
+ return key.key
+}
+
+func (key rsaSingingKey) VerifyKey() any {
+ return key.key.Public()
+}
+
+func (key rsaSingingKey) ToJWK() (map[string]string, error) {
+ pubKey := key.key.Public().(*rsa.PublicKey)
+
+ return map[string]string{
+ "kty": "RSA",
+ "alg": key.SigningMethod().Alg(),
+ "kid": key.id,
+ "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
+ "n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
+ }, nil
+}
+
+func (key rsaSingingKey) PreProcessToken(token *jwt.Token) {
+ token.Header["kid"] = key.id
+}
+
+type eddsaSigningKey struct {
+ signingMethod jwt.SigningMethod
+ key ed25519.PrivateKey
+ id string
+}
+
+func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) {
+ kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey))
+ if err != nil {
+ return eddsaSigningKey{}, err
+ }
+
+ return eddsaSigningKey{
+ signingMethod,
+ key,
+ base64.RawURLEncoding.EncodeToString(kid),
+ }, nil
+}
+
+func (key eddsaSigningKey) IsSymmetric() bool {
+ return false
+}
+
+func (key eddsaSigningKey) SigningMethod() jwt.SigningMethod {
+ return key.signingMethod
+}
+
+func (key eddsaSigningKey) SignKey() any {
+ return key.key
+}
+
+func (key eddsaSigningKey) VerifyKey() any {
+ return key.key.Public()
+}
+
+func (key eddsaSigningKey) ToJWK() (map[string]string, error) {
+ pubKey := key.key.Public().(ed25519.PublicKey)
+
+ return map[string]string{
+ "alg": key.SigningMethod().Alg(),
+ "kid": key.id,
+ "kty": "OKP",
+ "crv": "Ed25519",
+ "x": base64.RawURLEncoding.EncodeToString(pubKey),
+ }, nil
+}
+
+func (key eddsaSigningKey) PreProcessToken(token *jwt.Token) {
+ token.Header["kid"] = key.id
+}
+
+type ecdsaSingingKey struct {
+ signingMethod jwt.SigningMethod
+ key *ecdsa.PrivateKey
+ id string
+}
+
+func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
+ kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey))
+ if err != nil {
+ return ecdsaSingingKey{}, err
+ }
+
+ return ecdsaSingingKey{
+ signingMethod,
+ key,
+ base64.RawURLEncoding.EncodeToString(kid),
+ }, nil
+}
+
+func (key ecdsaSingingKey) IsSymmetric() bool {
+ return false
+}
+
+func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
+ return key.signingMethod
+}
+
+func (key ecdsaSingingKey) SignKey() any {
+ return key.key
+}
+
+func (key ecdsaSingingKey) VerifyKey() any {
+ return key.key.Public()
+}
+
+func (key ecdsaSingingKey) ToJWK() (map[string]string, error) {
+ pubKey := key.key.Public().(*ecdsa.PublicKey)
+
+ return map[string]string{
+ "kty": "EC",
+ "alg": key.SigningMethod().Alg(),
+ "kid": key.id,
+ "crv": pubKey.Params().Name,
+ "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()),
+ "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()),
+ }, nil
+}
+
+func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
+ token.Header["kid"] = key.id
+}
+
+// CreateJWTSigningKey creates a signing key from an algorithm / key pair.
+func CreateJWTSigningKey(algorithm string, key any) (JWTSigningKey, error) {
+ var signingMethod jwt.SigningMethod
+ switch algorithm {
+ case "HS256":
+ signingMethod = jwt.SigningMethodHS256
+ case "HS384":
+ signingMethod = jwt.SigningMethodHS384
+ case "HS512":
+ signingMethod = jwt.SigningMethodHS512
+
+ case "RS256":
+ signingMethod = jwt.SigningMethodRS256
+ case "RS384":
+ signingMethod = jwt.SigningMethodRS384
+ case "RS512":
+ signingMethod = jwt.SigningMethodRS512
+
+ case "ES256":
+ signingMethod = jwt.SigningMethodES256
+ case "ES384":
+ signingMethod = jwt.SigningMethodES384
+ case "ES512":
+ signingMethod = jwt.SigningMethodES512
+ case "EdDSA":
+ signingMethod = jwt.SigningMethodEdDSA
+ default:
+ return nil, ErrInvalidAlgorithmType{algorithm}
+ }
+
+ switch signingMethod.(type) {
+ case *jwt.SigningMethodEd25519:
+ privateKey, ok := key.(ed25519.PrivateKey)
+ if !ok {
+ return nil, jwt.ErrInvalidKeyType
+ }
+ return newEdDSASingingKey(signingMethod, privateKey)
+ case *jwt.SigningMethodECDSA:
+ privateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ return nil, jwt.ErrInvalidKeyType
+ }
+ return newECDSASingingKey(signingMethod, privateKey)
+ case *jwt.SigningMethodRSA:
+ privateKey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ return nil, jwt.ErrInvalidKeyType
+ }
+ return newRSASingingKey(signingMethod, privateKey)
+ default:
+ secret, ok := key.([]byte)
+ if !ok {
+ return nil, jwt.ErrInvalidKeyType
+ }
+ return hmacSigningKey{signingMethod, secret}, nil
+ }
+}
+
+// DefaultSigningKey is the default signing key for JWTs.
+var DefaultSigningKey JWTSigningKey
+
+// InitSigningKey creates the default signing key from settings or creates a random key.
+func InitSigningKey() error {
+ var err error
+ var key any
+
+ switch setting.OAuth2.JWTSigningAlgorithm {
+ case "HS256":
+ fallthrough
+ case "HS384":
+ fallthrough
+ case "HS512":
+ key = setting.GetGeneralTokenSigningSecret()
+ case "RS256":
+ fallthrough
+ case "RS384":
+ fallthrough
+ case "RS512":
+ fallthrough
+ case "ES256":
+ fallthrough
+ case "ES384":
+ fallthrough
+ case "ES512":
+ fallthrough
+ case "EdDSA":
+ key, err = loadOrCreateAsymmetricKey()
+ default:
+ return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
+ }
+
+ if err != nil {
+ return fmt.Errorf("Error while loading or creating JWT key: %w", err)
+ }
+
+ signingKey, err := CreateJWTSigningKey(setting.OAuth2.JWTSigningAlgorithm, key)
+ if err != nil {
+ return err
+ }
+
+ DefaultSigningKey = signingKey
+
+ return nil
+}
+
+// loadOrCreateAsymmetricKey checks if the configured private key exists.
+// If it does not exist a new random key gets generated and saved on the configured path.
+func loadOrCreateAsymmetricKey() (any, error) {
+ keyPath := setting.OAuth2.JWTSigningPrivateKeyFile
+
+ isExist, err := util.IsExist(keyPath)
+ if err != nil {
+ log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err)
+ }
+ if !isExist {
+ err := func() error {
+ key, err := func() (any, error) {
+ switch {
+ case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"):
+ return rsa.GenerateKey(rand.Reader, 4096)
+ case setting.OAuth2.JWTSigningAlgorithm == "EdDSA":
+ _, pk, err := ed25519.GenerateKey(rand.Reader)
+ return pk, err
+ default:
+ return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ }
+ }()
+ if err != nil {
+ return err
+ }
+
+ bytes, err := x509.MarshalPKCS8PrivateKey(key)
+ if err != nil {
+ return err
+ }
+
+ privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes}
+
+ if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
+ return err
+ }
+
+ f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err = f.Close(); err != nil {
+ log.Error("Close: %v", err)
+ }
+ }()
+
+ return pem.Encode(f, privateKeyPEM)
+ }()
+ if err != nil {
+ log.Fatal("Error generating private key: %v", err)
+ return nil, err
+ }
+ }
+
+ bytes, err := os.ReadFile(keyPath)
+ if err != nil {
+ return nil, err
+ }
+
+ block, _ := pem.Decode(bytes)
+ if block == nil {
+ return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
+ } else if block.Type != "PRIVATE KEY" {
+ return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath)
+ }
+
+ return x509.ParsePKCS8PrivateKey(block.Bytes)
+}
--- /dev/null
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package oauth2_provider //nolint
+
+import (
+ "fmt"
+ "time"
+
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// Token represents an Oauth grant
+
+// TokenKind represents the type of token for an oauth application
+type TokenKind int
+
+const (
+ // KindAccessToken is a token with short lifetime to access the api
+ KindAccessToken TokenKind = 0
+ // KindRefreshToken is token with long lifetime to refresh access tokens obtained by the client
+ KindRefreshToken = iota
+)
+
+// Token represents a JWT token used to authenticate a client
+type Token struct {
+ GrantID int64 `json:"gnt"`
+ Kind TokenKind `json:"tt"`
+ Counter int64 `json:"cnt,omitempty"`
+ jwt.RegisteredClaims
+}
+
+// ParseToken parses a signed jwt string
+func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
+ parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (any, error) {
+ if token.Method == nil || token.Method.Alg() != signingKey.SigningMethod().Alg() {
+ return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
+ }
+ return signingKey.VerifyKey(), nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ if !parsedToken.Valid {
+ return nil, fmt.Errorf("invalid token")
+ }
+ var token *Token
+ var ok bool
+ if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
+ return nil, fmt.Errorf("invalid token")
+ }
+ return token, nil
+}
+
+// SignToken signs the token with the JWT secret
+func (token *Token) SignToken(signingKey JWTSigningKey) (string, error) {
+ token.IssuedAt = jwt.NewNumericDate(time.Now())
+ jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
+ signingKey.PreProcessToken(jwtToken)
+ return jwtToken.SignedString(signingKey.SignKey())
+}
+
+// OIDCToken represents an OpenID Connect id_token
+type OIDCToken struct {
+ jwt.RegisteredClaims
+ Nonce string `json:"nonce,omitempty"`
+
+ // Scope profile
+ Name string `json:"name,omitempty"`
+ PreferredUsername string `json:"preferred_username,omitempty"`
+ Profile string `json:"profile,omitempty"`
+ Picture string `json:"picture,omitempty"`
+ Website string `json:"website,omitempty"`
+ Locale string `json:"locale,omitempty"`
+ UpdatedAt timeutil.TimeStamp `json:"updated_at,omitempty"`
+
+ // Scope email
+ Email string `json:"email,omitempty"`
+ EmailVerified bool `json:"email_verified,omitempty"`
+
+ // Groups are generated by organization and team names
+ Groups []string `json:"groups,omitempty"`
+}
+
+// SignToken signs an id_token with the (symmetric) client secret key
+func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
+ token.IssuedAt = jwt.NewNumericDate(time.Now())
+ jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
+ signingKey.PreProcessToken(jwtToken)
+ return jwtToken.SignedString(signingKey.SignKey())
+}
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/routers/web/auth"
+ oauth2_provider "code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"code": "authcode",
})
resp := MakeRequest(t, req, http.StatusBadRequest)
- parsedError := new(auth.AccessTokenError)
+ parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp := MakeRequest(t, req, http.StatusBadRequest)
- parsedError := new(auth.AccessTokenError)
+ parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "cannot load client with client id: '???'", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unexpected redirect URI", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "client is not authorized", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unsupported_grant_type", string(parsedError.ErrorCode))
assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription)
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OmJsYWJsYQ==")
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError := new(auth.AccessTokenError)
+ parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "cannot load client with client id: ''", parsedError.ErrorDescription)
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription)
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
"refresh_token": parsed.RefreshToken,
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError := new(auth.AccessTokenError)
+ parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid empty client secret", parsedError.ErrorDescription)
"refresh_token": "UNEXPECTED",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
// repeat request should fail
req.Body = io.NopCloser(bytes.NewReader(bs))
resp = MakeRequest(t, req, http.StatusBadRequest)
- parsedError = new(auth.AccessTokenError)
+ parsedError = new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "token was already used", parsedError.ErrorDescription)