From 63ab92d7971e4931e98f014f2c5385d2242fa780 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Wed, 9 Aug 2023 14:24:07 +0200 Subject: [PATCH] Pre-register OAuth2 applications for git credential helpers (#26291) This PR is an extended implementation of #25189 and builds upon the proposal by @hickford in #25653, utilizing some ideas proposed internally by @wxiaoguang. Mainly, this PR consists of a mechanism to pre-register OAuth2 applications on startup, which can be enabled or disabled by modifying the `[oauth2].DEFAULT_APPLICATIONS` parameter in app.ini. The OAuth2 applications registered this way are being marked as "locked" and neither be deleted nor edited over UI to prevent confusing/unexpected behavior. Instead, they're being removed if no longer enabled in config. ![grafik](https://github.com/go-gitea/gitea/assets/47871822/81a78b1c-4b68-40a7-9e99-c272ebb8f62e) The implemented mechanism can also be used to pre-register other OAuth2 applications in the future, if wanted. Co-authored-by: hickford Co-authored-by: wxiaoguang --------- Co-authored-by: M Hickford Co-authored-by: wxiaoguang --- custom/conf/app.example.ini | 5 + .../config-cheat-sheet.en-us.md | 1 + .../development/oauth2-provider.en-us.md | 11 +++ models/auth/oauth2.go | 91 +++++++++++++++++++ modules/setting/oauth2.go | 2 + options/locale/locale_en-US.ini | 2 + routers/init.go | 2 + routers/web/admin/applications.go | 2 +- routers/web/repo/http.go | 2 +- .../settings/applications_oauth2_list.tmpl | 25 +++-- 10 files changed, 131 insertions(+), 12 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 94c15a60d9c6..cfaf91cddb7f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -544,6 +544,11 @@ ENABLE = true ;; ;; Maximum length of oauth2 token/cookie stored on server ;MAX_TOKEN_LENGTH = 32767 +;; +;; Pre-register OAuth2 applications for some universally useful services +;; * https://github.com/hickford/git-credential-oauth +;; * https://github.com/git-ecosystem/git-credential-manager +;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 30751bf0711d..71ae4f2e30bd 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -1100,6 +1100,7 @@ This section only does "set" config, a removed config key from this section won' - `JWT_SECRET_URI`: **_empty_**: Instead of defining JWT_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the secret (example value: `file:/etc/gitea/oauth2_jwt_secret`) - `JWT_SIGNING_PRIVATE_KEY_FILE`: **jwt/private.pem**: Private key file path used to sign OAuth2 tokens. The path is relative to `APP_DATA_PATH`. This setting is only needed if `JWT_SIGNING_ALGORITHM` is set to `RS256`, `RS384`, `RS512`, `ES256`, `ES384` or `ES512`. The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you. - `MAX_TOKEN_LENGTH`: **32767**: Maximum length of token/cookie to accept from OAuth2 provider +- `DEFAULT_APPLICATIONS`: **git-credential-oauth, git-credential-manager**: Pre-register OAuth applications for some services on startup. See the [OAuth2 documentation](/development/oauth2-provider.md) for the list of available options. ## i18n (`i18n`) diff --git a/docs/content/development/oauth2-provider.en-us.md b/docs/content/development/oauth2-provider.en-us.md index b3824f4b2eac..81fc04bdcf1d 100644 --- a/docs/content/development/oauth2-provider.en-us.md +++ b/docs/content/development/oauth2-provider.en-us.md @@ -78,6 +78,17 @@ Gitea token scopes are as follows: |     **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. | |     **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings. | +## Pre-configured Applications + +Gitea creates OAuth applications for the following services by default on startup, as we assume that these are universally useful. + +|Application|Description|Client ID| +|-----------|-----------|---------| +|[git-credential-oauth](https://github.com/hickford/git-credential-oauth)|Git credential helper|`a4792ccc-144e-407e-86c9-5e7d8d9c3269`| +|[Git Credential Manager](https://github.com/git-ecosystem/git-credential-manager)|Git credential helper|`e90ee53c-94e2-48ac-9358-a874fb9e0662`| + +To prevent unexpected behavior, they are being displayed as locked in the UI and their creation can instead be controlled by the `DEFAULT_APPLICATIONS` parameter in `app.ini`. + ## Client types Gitea supports both confidential and public client types, [as defined by RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index 0f64b56c1635..1b6d68879a97 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -13,6 +13,8 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -46,6 +48,83 @@ func init() { db.RegisterModel(new(OAuth2Grant)) } +type BuiltinOAuth2Application struct { + ConfigName string + DisplayName string + RedirectURIs []string +} + +func BuiltinApplications() map[string]*BuiltinOAuth2Application { + m := make(map[string]*BuiltinOAuth2Application) + m["a4792ccc-144e-407e-86c9-5e7d8d9c3269"] = &BuiltinOAuth2Application{ + ConfigName: "git-credential-oauth", + DisplayName: "git-credential-oauth", + RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"}, + } + m["e90ee53c-94e2-48ac-9358-a874fb9e0662"] = &BuiltinOAuth2Application{ + ConfigName: "git-credential-manager", + DisplayName: "Git Credential Manager", + RedirectURIs: []string{"http://127.0.0.1", "https://127.0.0.1"}, + } + return m +} + +func Init(ctx context.Context) error { + builtinApps := BuiltinApplications() + var builtinAllClientIDs []string + for clientID := range builtinApps { + builtinAllClientIDs = append(builtinAllClientIDs, clientID) + } + + var registeredApps []*OAuth2Application + if err := db.GetEngine(ctx).In("client_id", builtinAllClientIDs).Find(®isteredApps); err != nil { + return err + } + + clientIDsToAdd := container.Set[string]{} + for _, configName := range setting.OAuth2.DefaultApplications { + found := false + for clientID, builtinApp := range builtinApps { + if builtinApp.ConfigName == configName { + clientIDsToAdd.Add(clientID) // add all user-configured apps to the "add" list + found = true + } + } + if !found { + return fmt.Errorf("unknown oauth2 application: %q", configName) + } + } + clientIDsToDelete := container.Set[string]{} + for _, app := range registeredApps { + if !clientIDsToAdd.Contains(app.ClientID) { + clientIDsToDelete.Add(app.ClientID) // if a registered app is not in the "add" list, it should be deleted + } + } + for _, app := range registeredApps { + clientIDsToAdd.Remove(app.ClientID) // no need to re-add existing (registered) apps, so remove them from the set + } + + for _, app := range registeredApps { + if clientIDsToDelete.Contains(app.ClientID) { + if err := deleteOAuth2Application(ctx, app.ID, 0); err != nil { + return err + } + } + } + for clientID := range clientIDsToAdd { + builtinApp := builtinApps[clientID] + if err := db.Insert(ctx, &OAuth2Application{ + Name: builtinApp.DisplayName, + ClientID: clientID, + RedirectURIs: builtinApp.RedirectURIs, + }); err != nil { + return err + } + } + + return nil +} + // TableName sets the table name to `oauth2_application` func (app *OAuth2Application) TableName() string { return "oauth2_application" @@ -205,6 +284,10 @@ func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) (*OAuth2Applic if app.UID != opts.UserID { return nil, fmt.Errorf("UID mismatch") } + builtinApps := BuiltinApplications() + if _, builtin := builtinApps[app.ClientID]; builtin { + return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID) + } app.Name = opts.Name app.RedirectURIs = opts.RedirectURIs @@ -261,6 +344,14 @@ func DeleteOAuth2Application(id, userid int64) error { return err } defer committer.Close() + app, err := GetOAuth2ApplicationByID(ctx, id) + if err != nil { + return err + } + builtinApps := BuiltinApplications() + if _, builtin := builtinApps[app.ClientID]; builtin { + return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID) + } if err := deleteOAuth2Application(ctx, id, userid); err != nil { return err } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 78a9462de9a6..b88070681a7d 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -100,6 +100,7 @@ var OAuth2 = struct { JWTSecretBase64 string `ini:"JWT_SECRET"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` MaxTokenLength int + DefaultApplications []string }{ Enable: true, AccessTokenExpirationTime: 3600, @@ -108,6 +109,7 @@ var OAuth2 = struct { JWTSigningAlgorithm: "RS256", JWTSigningPrivateKeyFile: "jwt/private.pem", MaxTokenLength: math.MaxInt16, + DefaultApplications: []string{"git-credential-oauth", "git-credential-manager"}, } func loadOAuth2From(rootCfg ConfigProvider) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 7697c2a61cc5..2023dade3fa4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -93,6 +93,7 @@ edit = Edit enabled = Enabled disabled = Disabled +locked = Locked copy = Copy copy_url = Copy URL @@ -850,6 +851,7 @@ oauth2_client_secret_hint = The secret will not be shown again after you leave o oauth2_application_edit = Edit oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance. oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue? +oauth2_application_locked = Gitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected bahavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information. authorized_oauth2_applications = Authorized OAuth2 Applications authorized_oauth2_applications_description = You have granted access to your personal Gitea account to these third party applications. Please revoke access for applications you no longer need. diff --git a/routers/init.go b/routers/init.go index ddbabcc39744..020fff31c0e3 100644 --- a/routers/init.go +++ b/routers/init.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" + authmodel "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/git" @@ -138,6 +139,7 @@ func InitWebInstalled(ctx context.Context) { mustInit(oauth2.Init) mustInitCtx(ctx, models.Init) + mustInitCtx(ctx, authmodel.Init) mustInit(repo_service.Init) // Booting long running goroutines. diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go index 7b2752434037..b26912db4821 100644 --- a/routers/web/admin/applications.go +++ b/routers/web/admin/applications.go @@ -39,7 +39,7 @@ func Applications(ctx *context.Context) { return } ctx.Data["Applications"] = apps - + ctx.Data["BuiltinApplications"] = auth.BuiltinApplications() ctx.HTML(http.StatusOK, tplSettingsApplications) } diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index 0cae9aeda454..c8ecb3b1d8ad 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -147,7 +147,7 @@ func httpBase(ctx *context.Context) *serviceHandler { // rely on the results of Contexter if !ctx.IsSigned { // TODO: support digit auth - which would be Authorization header with digit - ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"") + ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`) ctx.Error(http.StatusUnauthorized) return nil } diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl index be6569c03c03..1a536e50ac0a 100644 --- a/templates/user/settings/applications_oauth2_list.tmpl +++ b/templates/user/settings/applications_oauth2_list.tmpl @@ -4,7 +4,7 @@ {{.locale.Tr "settings.oauth2_application_create_description"}} {{range .Applications}} -
+
{{svg "octicon-apps" 32}}
@@ -15,16 +15,21 @@ {{.ClientID}}
+ {{$isBuiltin := and $.BuiltinApplications (index $.BuiltinApplications .ClientID)}}
- - {{svg "octicon-pencil" 16 "gt-mr-2"}} - {{$.locale.Tr "settings.oauth2_application_edit"}} - - + {{if $isBuiltin}} + {{ctx.Locale.Tr "locked"}} + {{else}} + + {{svg "octicon-pencil" 16 "gt-mr-2"}} + {{$.locale.Tr "settings.oauth2_application_edit"}} + + + {{end}}
{{end}}