mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 09:31:53 +01:00 
			
		
		
		
	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.  The implemented mechanism can also be used to pre-register other OAuth2 applications in the future, if wanted. Co-authored-by: hickford <mirth.hickford@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> --------- Co-authored-by: M Hickford <mirth.hickford@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		
							parent
							
								
									d41aee1d1e
								
							
						
					
					
						commit
						63ab92d797
					
				| @ -544,6 +544,11 @@ ENABLE = true | |||||||
| ;; | ;; | ||||||
| ;; Maximum length of oauth2 token/cookie stored on server | ;; Maximum length of oauth2 token/cookie stored on server | ||||||
| ;MAX_TOKEN_LENGTH = 32767 | ;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 | ||||||
| 
 | 
 | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|  | |||||||
| @ -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_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. | - `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 | - `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`) | ## i18n (`i18n`) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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.                                                    | | |     **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.                     | | |     **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 | ## 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). | Gitea supports both confidential and public client types, [as defined by RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). | ||||||
|  | |||||||
| @ -13,6 +13,8 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"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/timeutil" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 
 | 
 | ||||||
| @ -46,6 +48,83 @@ func init() { | |||||||
| 	db.RegisterModel(new(OAuth2Grant)) | 	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` | // TableName sets the table name to `oauth2_application` | ||||||
| func (app *OAuth2Application) TableName() string { | func (app *OAuth2Application) TableName() string { | ||||||
| 	return "oauth2_application" | 	return "oauth2_application" | ||||||
| @ -205,6 +284,10 @@ func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) (*OAuth2Applic | |||||||
| 	if app.UID != opts.UserID { | 	if app.UID != opts.UserID { | ||||||
| 		return nil, fmt.Errorf("UID mismatch") | 		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.Name = opts.Name | ||||||
| 	app.RedirectURIs = opts.RedirectURIs | 	app.RedirectURIs = opts.RedirectURIs | ||||||
| @ -261,6 +344,14 @@ func DeleteOAuth2Application(id, userid int64) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	defer committer.Close() | 	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 { | 	if err := deleteOAuth2Application(ctx, id, userid); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -100,6 +100,7 @@ var OAuth2 = struct { | |||||||
| 	JWTSecretBase64            string `ini:"JWT_SECRET"` | 	JWTSecretBase64            string `ini:"JWT_SECRET"` | ||||||
| 	JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` | 	JWTSigningPrivateKeyFile   string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` | ||||||
| 	MaxTokenLength             int | 	MaxTokenLength             int | ||||||
|  | 	DefaultApplications        []string | ||||||
| }{ | }{ | ||||||
| 	Enable:                     true, | 	Enable:                     true, | ||||||
| 	AccessTokenExpirationTime:  3600, | 	AccessTokenExpirationTime:  3600, | ||||||
| @ -108,6 +109,7 @@ var OAuth2 = struct { | |||||||
| 	JWTSigningAlgorithm:        "RS256", | 	JWTSigningAlgorithm:        "RS256", | ||||||
| 	JWTSigningPrivateKeyFile:   "jwt/private.pem", | 	JWTSigningPrivateKeyFile:   "jwt/private.pem", | ||||||
| 	MaxTokenLength:             math.MaxInt16, | 	MaxTokenLength:             math.MaxInt16, | ||||||
|  | 	DefaultApplications:        []string{"git-credential-oauth", "git-credential-manager"}, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func loadOAuth2From(rootCfg ConfigProvider) { | func loadOAuth2From(rootCfg ConfigProvider) { | ||||||
|  | |||||||
| @ -93,6 +93,7 @@ edit = Edit | |||||||
| 
 | 
 | ||||||
| enabled = Enabled | enabled = Enabled | ||||||
| disabled = Disabled | disabled = Disabled | ||||||
|  | locked = Locked | ||||||
| 
 | 
 | ||||||
| copy = Copy | copy = Copy | ||||||
| copy_url = Copy URL | 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_edit = Edit | ||||||
| oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance. | 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_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 = 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. | 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. | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ import ( | |||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	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/cache" | ||||||
| 	"code.gitea.io/gitea/modules/eventsource" | 	"code.gitea.io/gitea/modules/eventsource" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| @ -138,6 +139,7 @@ func InitWebInstalled(ctx context.Context) { | |||||||
| 	mustInit(oauth2.Init) | 	mustInit(oauth2.Init) | ||||||
| 
 | 
 | ||||||
| 	mustInitCtx(ctx, models.Init) | 	mustInitCtx(ctx, models.Init) | ||||||
|  | 	mustInitCtx(ctx, authmodel.Init) | ||||||
| 	mustInit(repo_service.Init) | 	mustInit(repo_service.Init) | ||||||
| 
 | 
 | ||||||
| 	// Booting long running goroutines. | 	// Booting long running goroutines. | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ func Applications(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["Applications"] = apps | 	ctx.Data["Applications"] = apps | ||||||
| 
 | 	ctx.Data["BuiltinApplications"] = auth.BuiltinApplications() | ||||||
| 	ctx.HTML(http.StatusOK, tplSettingsApplications) | 	ctx.HTML(http.StatusOK, tplSettingsApplications) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -147,7 +147,7 @@ func httpBase(ctx *context.Context) *serviceHandler { | |||||||
| 		// rely on the results of Contexter | 		// rely on the results of Contexter | ||||||
| 		if !ctx.IsSigned { | 		if !ctx.IsSigned { | ||||||
| 			// TODO: support digit auth - which would be Authorization header with digit | 			// 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) | 			ctx.Error(http.StatusUnauthorized) | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ | |||||||
| 			{{.locale.Tr "settings.oauth2_application_create_description"}} | 			{{.locale.Tr "settings.oauth2_application_create_description"}} | ||||||
| 		</div> | 		</div> | ||||||
| 		{{range .Applications}} | 		{{range .Applications}} | ||||||
| 			<div class="flex-item"> | 			<div class="flex-item flex-item-center"> | ||||||
| 				<div class="flex-item-leading"> | 				<div class="flex-item-leading"> | ||||||
| 					{{svg "octicon-apps" 32}} | 					{{svg "octicon-apps" 32}} | ||||||
| 				</div> | 				</div> | ||||||
| @ -15,16 +15,21 @@ | |||||||
| 						<span class="ui label">{{.ClientID}}</span> | 						<span class="ui label">{{.ClientID}}</span> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  | 				{{$isBuiltin := and $.BuiltinApplications (index $.BuiltinApplications .ClientID)}} | ||||||
| 				<div class="flex-item-trailing"> | 				<div class="flex-item-trailing"> | ||||||
| 					<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button"> | 					{{if $isBuiltin}} | ||||||
| 						{{svg "octicon-pencil" 16 "gt-mr-2"}} | 						<span class="ui basic label" data-tooltip-content="{{$.locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span> | ||||||
| 						{{$.locale.Tr "settings.oauth2_application_edit"}} | 					{{else}} | ||||||
| 					</a> | 						<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button"> | ||||||
| 					<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" | 							{{svg "octicon-pencil" 16 "gt-mr-2"}} | ||||||
| 							data-url="{{$.Link}}/oauth2/{{.ID}}/delete"> | 							{{$.locale.Tr "settings.oauth2_application_edit"}} | ||||||
| 						{{svg "octicon-trash" 16 "gt-mr-2"}} | 						</a> | ||||||
| 						{{$.locale.Tr "settings.delete_key"}} | 						<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" | ||||||
| 					</button> | 								data-url="{{$.Link}}/oauth2/{{.ID}}/delete"> | ||||||
|  | 							{{svg "octicon-trash" 16 "gt-mr-2"}} | ||||||
|  | 							{{$.locale.Tr "settings.delete_key"}} | ||||||
|  | 						</button> | ||||||
|  | 					{{end}} | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		{{end}} | 		{{end}} | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user