mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 16:01:32 +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 | ||||
| ;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_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`) | ||||
| 
 | ||||
|  | ||||
| @ -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). | ||||
|  | ||||
| @ -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 | ||||
| 	} | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -39,7 +39,7 @@ func Applications(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["Applications"] = apps | ||||
| 
 | ||||
| 	ctx.Data["BuiltinApplications"] = auth.BuiltinApplications() | ||||
| 	ctx.HTML(http.StatusOK, tplSettingsApplications) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
| 		} | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| 			{{.locale.Tr "settings.oauth2_application_create_description"}} | ||||
| 		</div> | ||||
| 		{{range .Applications}} | ||||
| 			<div class="flex-item"> | ||||
| 			<div class="flex-item flex-item-center"> | ||||
| 				<div class="flex-item-leading"> | ||||
| 					{{svg "octicon-apps" 32}} | ||||
| 				</div> | ||||
| @ -15,16 +15,21 @@ | ||||
| 						<span class="ui label">{{.ClientID}}</span> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				{{$isBuiltin := and $.BuiltinApplications (index $.BuiltinApplications .ClientID)}} | ||||
| 				<div class="flex-item-trailing"> | ||||
| 					<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button"> | ||||
| 						{{svg "octicon-pencil" 16 "gt-mr-2"}} | ||||
| 						{{$.locale.Tr "settings.oauth2_application_edit"}} | ||||
| 					</a> | ||||
| 					<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" | ||||
| 							data-url="{{$.Link}}/oauth2/{{.ID}}/delete"> | ||||
| 						{{svg "octicon-trash" 16 "gt-mr-2"}} | ||||
| 						{{$.locale.Tr "settings.delete_key"}} | ||||
| 					</button> | ||||
| 					{{if $isBuiltin}} | ||||
| 						<span class="ui basic label" data-tooltip-content="{{$.locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span> | ||||
| 					{{else}} | ||||
| 						<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button"> | ||||
| 							{{svg "octicon-pencil" 16 "gt-mr-2"}} | ||||
| 							{{$.locale.Tr "settings.oauth2_application_edit"}} | ||||
| 						</a> | ||||
| 						<button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" | ||||
| 								data-url="{{$.Link}}/oauth2/{{.ID}}/delete"> | ||||
| 							{{svg "octicon-trash" 16 "gt-mr-2"}} | ||||
| 							{{$.locale.Tr "settings.delete_key"}} | ||||
| 						</button> | ||||
| 					{{end}} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{{end}} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user