mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	add skip secondary authorization option for public oauth2 clients (#31454)
This commit is contained in:
		| @@ -37,10 +37,11 @@ type OAuth2Application struct { | ||||
| 	// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 | ||||
| 	// "Authorization servers MUST record the client type in the client registration details" | ||||
| 	// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 | ||||
| 	ConfidentialClient bool               `xorm:"NOT NULL DEFAULT TRUE"` | ||||
| 	RedirectURIs       []string           `xorm:"redirect_uris JSON TEXT"` | ||||
| 	CreatedUnix        timeutil.TimeStamp `xorm:"INDEX created"` | ||||
| 	UpdatedUnix        timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
| 	ConfidentialClient         bool               `xorm:"NOT NULL DEFAULT TRUE"` | ||||
| 	SkipSecondaryAuthorization bool               `xorm:"NOT NULL DEFAULT FALSE"` | ||||
| 	RedirectURIs               []string           `xorm:"redirect_uris JSON TEXT"` | ||||
| 	CreatedUnix                timeutil.TimeStamp `xorm:"INDEX created"` | ||||
| 	UpdatedUnix                timeutil.TimeStamp `xorm:"INDEX updated"` | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| @@ -251,21 +252,23 @@ func GetOAuth2ApplicationByID(ctx context.Context, id int64) (app *OAuth2Applica | ||||
|  | ||||
| // CreateOAuth2ApplicationOptions holds options to create an oauth2 application | ||||
| type CreateOAuth2ApplicationOptions struct { | ||||
| 	Name               string | ||||
| 	UserID             int64 | ||||
| 	ConfidentialClient bool | ||||
| 	RedirectURIs       []string | ||||
| 	Name                       string | ||||
| 	UserID                     int64 | ||||
| 	ConfidentialClient         bool | ||||
| 	SkipSecondaryAuthorization bool | ||||
| 	RedirectURIs               []string | ||||
| } | ||||
|  | ||||
| // CreateOAuth2Application inserts a new oauth2 application | ||||
| func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) { | ||||
| 	clientID := uuid.New().String() | ||||
| 	app := &OAuth2Application{ | ||||
| 		UID:                opts.UserID, | ||||
| 		Name:               opts.Name, | ||||
| 		ClientID:           clientID, | ||||
| 		RedirectURIs:       opts.RedirectURIs, | ||||
| 		ConfidentialClient: opts.ConfidentialClient, | ||||
| 		UID:                        opts.UserID, | ||||
| 		Name:                       opts.Name, | ||||
| 		ClientID:                   clientID, | ||||
| 		RedirectURIs:               opts.RedirectURIs, | ||||
| 		ConfidentialClient:         opts.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: opts.SkipSecondaryAuthorization, | ||||
| 	} | ||||
| 	if err := db.Insert(ctx, app); err != nil { | ||||
| 		return nil, err | ||||
| @@ -275,11 +278,12 @@ func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOp | ||||
|  | ||||
| // UpdateOAuth2ApplicationOptions holds options to update an oauth2 application | ||||
| type UpdateOAuth2ApplicationOptions struct { | ||||
| 	ID                 int64 | ||||
| 	Name               string | ||||
| 	UserID             int64 | ||||
| 	ConfidentialClient bool | ||||
| 	RedirectURIs       []string | ||||
| 	ID                         int64 | ||||
| 	Name                       string | ||||
| 	UserID                     int64 | ||||
| 	ConfidentialClient         bool | ||||
| 	SkipSecondaryAuthorization bool | ||||
| 	RedirectURIs               []string | ||||
| } | ||||
|  | ||||
| // UpdateOAuth2Application updates an oauth2 application | ||||
| @@ -305,6 +309,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp | ||||
| 	app.Name = opts.Name | ||||
| 	app.RedirectURIs = opts.RedirectURIs | ||||
| 	app.ConfidentialClient = opts.ConfidentialClient | ||||
| 	app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization | ||||
|  | ||||
| 	if err = updateOAuth2Application(ctx, app); err != nil { | ||||
| 		return nil, err | ||||
| @@ -315,7 +320,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp | ||||
| } | ||||
|  | ||||
| func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error { | ||||
| 	if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil { | ||||
| 	if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client", "skip_secondary_authorization").Update(app); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
|   | ||||
| @@ -593,6 +593,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment), | ||||
| 	// v300 -> v301 | ||||
| 	NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection), | ||||
| 	// v301 -> v302 | ||||
| 	NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable), | ||||
| } | ||||
|  | ||||
| // GetCurrentDBVersion returns the current db version | ||||
|   | ||||
							
								
								
									
										14
									
								
								models/migrations/v1_23/v301.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								models/migrations/v1_23/v301.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // Copyright 2024 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| package v1_23 //nolint | ||||
|  | ||||
| import "xorm.io/xorm" | ||||
|  | ||||
| // AddSkipSeconderyAuthToOAuth2ApplicationTable: add SkipSecondaryAuthorization column, setting existing rows to false | ||||
| func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error { | ||||
| 	type oauth2Application struct { | ||||
| 		SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"` | ||||
| 	} | ||||
| 	return x.Sync(new(oauth2Application)) | ||||
| } | ||||
| @@ -31,21 +31,23 @@ type CreateAccessTokenOption struct { | ||||
|  | ||||
| // CreateOAuth2ApplicationOptions holds options to create an oauth2 application | ||||
| type CreateOAuth2ApplicationOptions struct { | ||||
| 	Name               string   `json:"name" binding:"Required"` | ||||
| 	ConfidentialClient bool     `json:"confidential_client"` | ||||
| 	RedirectURIs       []string `json:"redirect_uris" binding:"Required"` | ||||
| 	Name                       string   `json:"name" binding:"Required"` | ||||
| 	ConfidentialClient         bool     `json:"confidential_client"` | ||||
| 	SkipSecondaryAuthorization bool     `json:"skip_secondary_authorization"` | ||||
| 	RedirectURIs               []string `json:"redirect_uris" binding:"Required"` | ||||
| } | ||||
|  | ||||
| // OAuth2Application represents an OAuth2 application. | ||||
| // swagger:response OAuth2Application | ||||
| type OAuth2Application struct { | ||||
| 	ID                 int64     `json:"id"` | ||||
| 	Name               string    `json:"name"` | ||||
| 	ClientID           string    `json:"client_id"` | ||||
| 	ClientSecret       string    `json:"client_secret"` | ||||
| 	ConfidentialClient bool      `json:"confidential_client"` | ||||
| 	RedirectURIs       []string  `json:"redirect_uris"` | ||||
| 	Created            time.Time `json:"created"` | ||||
| 	ID                         int64     `json:"id"` | ||||
| 	Name                       string    `json:"name"` | ||||
| 	ClientID                   string    `json:"client_id"` | ||||
| 	ClientSecret               string    `json:"client_secret"` | ||||
| 	ConfidentialClient         bool      `json:"confidential_client"` | ||||
| 	SkipSecondaryAuthorization bool      `json:"skip_secondary_authorization"` | ||||
| 	RedirectURIs               []string  `json:"redirect_uris"` | ||||
| 	Created                    time.Time `json:"created"` | ||||
| } | ||||
|  | ||||
| // OAuth2ApplicationList represents a list of OAuth2 applications. | ||||
|   | ||||
| @@ -914,6 +914,7 @@ create_oauth2_application_success = You have successfully created a new OAuth2 a | ||||
| update_oauth2_application_success = You have successfully updated the OAuth2 application. | ||||
| oauth2_application_name = Application Name | ||||
| oauth2_confidential_client = Confidential Client. Select for apps that keep the secret confidential, such as web apps. Do not select for native apps including desktop and mobile apps. | ||||
| oauth2_skip_secondary_authorization = Skip authorization for public clients after granting access once. <strong>May pose a security risk.</strong> | ||||
| oauth2_redirect_uris = Redirect URIs. Please use a new line for every URI. | ||||
| save_application = Save | ||||
| oauth2_client_id = Client ID | ||||
|   | ||||
| @@ -223,10 +223,11 @@ func CreateOauth2Application(ctx *context.APIContext) { | ||||
| 	data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) | ||||
|  | ||||
| 	app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{ | ||||
| 		Name:               data.Name, | ||||
| 		UserID:             ctx.Doer.ID, | ||||
| 		RedirectURIs:       data.RedirectURIs, | ||||
| 		ConfidentialClient: data.ConfidentialClient, | ||||
| 		Name:                       data.Name, | ||||
| 		UserID:                     ctx.Doer.ID, | ||||
| 		RedirectURIs:               data.RedirectURIs, | ||||
| 		ConfidentialClient:         data.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: data.SkipSecondaryAuthorization, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application") | ||||
| @@ -381,11 +382,12 @@ func UpdateOauth2Application(ctx *context.APIContext) { | ||||
| 	data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) | ||||
|  | ||||
| 	app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{ | ||||
| 		Name:               data.Name, | ||||
| 		UserID:             ctx.Doer.ID, | ||||
| 		ID:                 appID, | ||||
| 		RedirectURIs:       data.RedirectURIs, | ||||
| 		ConfidentialClient: data.ConfidentialClient, | ||||
| 		Name:                       data.Name, | ||||
| 		UserID:                     ctx.Doer.ID, | ||||
| 		ID:                         appID, | ||||
| 		RedirectURIs:               data.RedirectURIs, | ||||
| 		ConfidentialClient:         data.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: data.SkipSecondaryAuthorization, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { | ||||
|   | ||||
| @@ -469,9 +469,9 @@ func AuthorizeOAuth(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Redirect if user already granted access and the application is confidential. | ||||
| 	// I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2 | ||||
| 	if app.ConfidentialClient && grant != nil { | ||||
| 	// 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) | ||||
|   | ||||
| @@ -49,10 +49,11 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) { | ||||
|  | ||||
| 	// TODO validate redirect URI | ||||
| 	app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{ | ||||
| 		Name:               form.Name, | ||||
| 		RedirectURIs:       util.SplitTrimSpace(form.RedirectURIs, "\n"), | ||||
| 		UserID:             oa.OwnerID, | ||||
| 		ConfidentialClient: form.ConfidentialClient, | ||||
| 		Name:                       form.Name, | ||||
| 		RedirectURIs:               util.SplitTrimSpace(form.RedirectURIs, "\n"), | ||||
| 		UserID:                     oa.OwnerID, | ||||
| 		ConfidentialClient:         form.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: form.SkipSecondaryAuthorization, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("CreateOAuth2Application", err) | ||||
| @@ -102,11 +103,12 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) { | ||||
| 	// TODO validate redirect URI | ||||
| 	var err error | ||||
| 	if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{ | ||||
| 		ID:                 ctx.PathParamInt64("id"), | ||||
| 		Name:               form.Name, | ||||
| 		RedirectURIs:       util.SplitTrimSpace(form.RedirectURIs, "\n"), | ||||
| 		UserID:             oa.OwnerID, | ||||
| 		ConfidentialClient: form.ConfidentialClient, | ||||
| 		ID:                         ctx.PathParamInt64("id"), | ||||
| 		Name:                       form.Name, | ||||
| 		RedirectURIs:               util.SplitTrimSpace(form.RedirectURIs, "\n"), | ||||
| 		UserID:                     oa.OwnerID, | ||||
| 		ConfidentialClient:         form.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: form.SkipSecondaryAuthorization, | ||||
| 	}); err != nil { | ||||
| 		ctx.ServerError("UpdateOAuth2Application", err) | ||||
| 		return | ||||
|   | ||||
| @@ -455,13 +455,14 @@ func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { | ||||
| // ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application | ||||
| func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { | ||||
| 	return &api.OAuth2Application{ | ||||
| 		ID:                 app.ID, | ||||
| 		Name:               app.Name, | ||||
| 		ClientID:           app.ClientID, | ||||
| 		ClientSecret:       app.ClientSecret, | ||||
| 		ConfidentialClient: app.ConfidentialClient, | ||||
| 		RedirectURIs:       app.RedirectURIs, | ||||
| 		Created:            app.CreatedUnix.AsTime(), | ||||
| 		ID:                         app.ID, | ||||
| 		Name:                       app.Name, | ||||
| 		ClientID:                   app.ClientID, | ||||
| 		ClientSecret:               app.ClientSecret, | ||||
| 		ConfidentialClient:         app.ConfidentialClient, | ||||
| 		SkipSecondaryAuthorization: app.SkipSecondaryAuthorization, | ||||
| 		RedirectURIs:               app.RedirectURIs, | ||||
| 		Created:                    app.CreatedUnix.AsTime(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -365,9 +365,10 @@ func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) { | ||||
|  | ||||
| // EditOAuth2ApplicationForm form for editing oauth2 applications | ||||
| type EditOAuth2ApplicationForm struct { | ||||
| 	Name               string `binding:"Required;MaxSize(255)" form:"application_name"` | ||||
| 	RedirectURIs       string `binding:"Required" form:"redirect_uris"` | ||||
| 	ConfidentialClient bool   `form:"confidential_client"` | ||||
| 	Name                       string `binding:"Required;MaxSize(255)" form:"application_name"` | ||||
| 	RedirectURIs               string `binding:"Required" form:"redirect_uris"` | ||||
| 	ConfidentialClient         bool   `form:"confidential_client"` | ||||
| 	SkipSecondaryAuthorization bool   `form:"skip_secondary_authorization"` | ||||
| } | ||||
|  | ||||
| // Validate validates the fields | ||||
|   | ||||
							
								
								
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -19875,6 +19875,10 @@ | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "x-go-name": "RedirectURIs" | ||||
|         }, | ||||
|         "skip_secondary_authorization": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "SkipSecondaryAuthorization" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
| @@ -23002,6 +23006,10 @@ | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "x-go-name": "RedirectURIs" | ||||
|         }, | ||||
|         "skip_secondary_authorization": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "SkipSecondaryAuthorization" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|   | ||||
| @@ -44,7 +44,13 @@ | ||||
| 		<div class="field {{if .Err_ConfidentialClient}}error{{end}}"> | ||||
| 			<div class="ui checkbox"> | ||||
| 				<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label> | ||||
| 				<input type="checkbox" name="confidential_client" {{if .App.ConfidentialClient}}checked{{end}}> | ||||
| 				<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" {{if .App.ConfidentialClient}}checked{{end}}> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} {{if .App.ConfidentialClient}}disabled{{end}}" id="skip-secondary-authorization"> | ||||
| 			<div class="ui checkbox"> | ||||
| 				<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label> | ||||
| 				<input type="checkbox" name="skip_secondary_authorization" {{if .App.SkipSecondaryAuthorization}}checked{{end}}> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<button class="ui primary button"> | ||||
|   | ||||
| @@ -64,7 +64,13 @@ | ||||
| 		<div class="field {{if .Err_ConfidentialClient}}error{{end}}"> | ||||
| 			<div class="ui checkbox"> | ||||
| 				<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label> | ||||
| 				<input type="checkbox" name="confidential_client" checked> | ||||
| 				<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization"> | ||||
| 			<div class="ui checkbox"> | ||||
| 				<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label> | ||||
| 				<input type="checkbox" name="skip_secondary_authorization"> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<button class="ui primary button"> | ||||
|   | ||||
							
								
								
									
										5
									
								
								web_src/js/features/oauth2-settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web_src/js/features/oauth2-settings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export function initOAuth2SettingsDisableCheckbox() { | ||||
|   for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => { | ||||
|     document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked); | ||||
|   }); | ||||
| } | ||||
| @@ -78,6 +78,7 @@ import {initDirAuto} from './modules/dirauto.ts'; | ||||
| import {initRepositorySearch} from './features/repo-search.ts'; | ||||
| import {initColorPickers} from './features/colorpicker.ts'; | ||||
| import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; | ||||
| import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; | ||||
| import {initGlobalFetchAction} from './features/common-fetch-action.ts'; | ||||
| import { | ||||
|   initFootLanguageMenu, | ||||
| @@ -225,5 +226,7 @@ onDomReady(() => { | ||||
|     initPdfViewer, | ||||
|     initScopedAccessTokenCategories, | ||||
|     initColorPickers, | ||||
|  | ||||
|     initOAuth2SettingsDisableCheckbox, | ||||
|   ]); | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user