add skip secondary authorization option for public oauth2 clients (#31454)

This commit is contained in:
Denys Konovalov 2024-07-19 14:28:30 -04:00 committed by GitHub
parent e9aa39bda4
commit a8d0c879c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 120 additions and 62 deletions

View File

@ -38,6 +38,7 @@ type OAuth2Application struct {
// "Authorization servers MUST record the client type in the client registration details" // "Authorization servers MUST record the client type in the client registration details"
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
RedirectURIs []string `xorm:"redirect_uris JSON TEXT"` RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@ -254,6 +255,7 @@ type CreateOAuth2ApplicationOptions struct {
Name string Name string
UserID int64 UserID int64
ConfidentialClient bool ConfidentialClient bool
SkipSecondaryAuthorization bool
RedirectURIs []string RedirectURIs []string
} }
@ -266,6 +268,7 @@ func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOp
ClientID: clientID, ClientID: clientID,
RedirectURIs: opts.RedirectURIs, RedirectURIs: opts.RedirectURIs,
ConfidentialClient: opts.ConfidentialClient, ConfidentialClient: opts.ConfidentialClient,
SkipSecondaryAuthorization: opts.SkipSecondaryAuthorization,
} }
if err := db.Insert(ctx, app); err != nil { if err := db.Insert(ctx, app); err != nil {
return nil, err return nil, err
@ -279,6 +282,7 @@ type UpdateOAuth2ApplicationOptions struct {
Name string Name string
UserID int64 UserID int64
ConfidentialClient bool ConfidentialClient bool
SkipSecondaryAuthorization bool
RedirectURIs []string RedirectURIs []string
} }
@ -305,6 +309,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
app.Name = opts.Name app.Name = opts.Name
app.RedirectURIs = opts.RedirectURIs app.RedirectURIs = opts.RedirectURIs
app.ConfidentialClient = opts.ConfidentialClient app.ConfidentialClient = opts.ConfidentialClient
app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization
if err = updateOAuth2Application(ctx, app); err != nil { if err = updateOAuth2Application(ctx, app); err != nil {
return nil, err return nil, err
@ -315,7 +320,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
} }
func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error { 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 err
} }
return nil return nil

View File

@ -593,6 +593,8 @@ var migrations = []Migration{
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment), NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
// v300 -> v301 // v300 -> v301
NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection), 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 // GetCurrentDBVersion returns the current db version

View 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))
}

View File

@ -33,6 +33,7 @@ type CreateAccessTokenOption struct {
type CreateOAuth2ApplicationOptions struct { type CreateOAuth2ApplicationOptions struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
ConfidentialClient bool `json:"confidential_client"` ConfidentialClient bool `json:"confidential_client"`
SkipSecondaryAuthorization bool `json:"skip_secondary_authorization"`
RedirectURIs []string `json:"redirect_uris" binding:"Required"` RedirectURIs []string `json:"redirect_uris" binding:"Required"`
} }
@ -44,6 +45,7 @@ type OAuth2Application struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
ConfidentialClient bool `json:"confidential_client"` ConfidentialClient bool `json:"confidential_client"`
SkipSecondaryAuthorization bool `json:"skip_secondary_authorization"`
RedirectURIs []string `json:"redirect_uris"` RedirectURIs []string `json:"redirect_uris"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
} }

View File

@ -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. update_oauth2_application_success = You have successfully updated the OAuth2 application.
oauth2_application_name = Application Name 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_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. oauth2_redirect_uris = Redirect URIs. Please use a new line for every URI.
save_application = Save save_application = Save
oauth2_client_id = Client ID oauth2_client_id = Client ID

View File

@ -227,6 +227,7 @@ func CreateOauth2Application(ctx *context.APIContext) {
UserID: ctx.Doer.ID, UserID: ctx.Doer.ID,
RedirectURIs: data.RedirectURIs, RedirectURIs: data.RedirectURIs,
ConfidentialClient: data.ConfidentialClient, ConfidentialClient: data.ConfidentialClient,
SkipSecondaryAuthorization: data.SkipSecondaryAuthorization,
}) })
if err != nil { if err != nil {
ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application") ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application")
@ -386,6 +387,7 @@ func UpdateOauth2Application(ctx *context.APIContext) {
ID: appID, ID: appID,
RedirectURIs: data.RedirectURIs, RedirectURIs: data.RedirectURIs,
ConfidentialClient: data.ConfidentialClient, ConfidentialClient: data.ConfidentialClient,
SkipSecondaryAuthorization: data.SkipSecondaryAuthorization,
}) })
if err != nil { if err != nil {
if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) {

View File

@ -469,9 +469,9 @@ func AuthorizeOAuth(ctx *context.Context) {
return return
} }
// Redirect if user already granted access and the application is confidential. // Redirect if user already granted access and the application is confidential or trusted otherwise
// I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2 // I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2
if app.ConfidentialClient && grant != nil { if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil {
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod) code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
if err != nil { if err != nil {
handleServerError(ctx, form.State, form.RedirectURI) handleServerError(ctx, form.State, form.RedirectURI)

View File

@ -53,6 +53,7 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"), RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID, UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient, ConfidentialClient: form.ConfidentialClient,
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
}) })
if err != nil { if err != nil {
ctx.ServerError("CreateOAuth2Application", err) ctx.ServerError("CreateOAuth2Application", err)
@ -107,6 +108,7 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"), RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
UserID: oa.OwnerID, UserID: oa.OwnerID,
ConfidentialClient: form.ConfidentialClient, ConfidentialClient: form.ConfidentialClient,
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
}); err != nil { }); err != nil {
ctx.ServerError("UpdateOAuth2Application", err) ctx.ServerError("UpdateOAuth2Application", err)
return return

View File

@ -460,6 +460,7 @@ func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
ClientID: app.ClientID, ClientID: app.ClientID,
ClientSecret: app.ClientSecret, ClientSecret: app.ClientSecret,
ConfidentialClient: app.ConfidentialClient, ConfidentialClient: app.ConfidentialClient,
SkipSecondaryAuthorization: app.SkipSecondaryAuthorization,
RedirectURIs: app.RedirectURIs, RedirectURIs: app.RedirectURIs,
Created: app.CreatedUnix.AsTime(), Created: app.CreatedUnix.AsTime(),
} }

View File

@ -368,6 +368,7 @@ type EditOAuth2ApplicationForm struct {
Name string `binding:"Required;MaxSize(255)" form:"application_name"` Name string `binding:"Required;MaxSize(255)" form:"application_name"`
RedirectURIs string `binding:"Required" form:"redirect_uris"` RedirectURIs string `binding:"Required" form:"redirect_uris"`
ConfidentialClient bool `form:"confidential_client"` ConfidentialClient bool `form:"confidential_client"`
SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"`
} }
// Validate validates the fields // Validate validates the fields

View File

@ -19875,6 +19875,10 @@
"type": "string" "type": "string"
}, },
"x-go-name": "RedirectURIs" "x-go-name": "RedirectURIs"
},
"skip_secondary_authorization": {
"type": "boolean",
"x-go-name": "SkipSecondaryAuthorization"
} }
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
@ -23002,6 +23006,10 @@
"type": "string" "type": "string"
}, },
"x-go-name": "RedirectURIs" "x-go-name": "RedirectURIs"
},
"skip_secondary_authorization": {
"type": "boolean",
"x-go-name": "SkipSecondaryAuthorization"
} }
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"

View File

@ -44,7 +44,13 @@
<div class="field {{if .Err_ConfidentialClient}}error{{end}}"> <div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox"> <div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label> <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>
</div> </div>
<button class="ui primary button"> <button class="ui primary button">

View File

@ -64,7 +64,13 @@
<div class="field {{if .Err_ConfidentialClient}}error{{end}}"> <div class="field {{if .Err_ConfidentialClient}}error{{end}}">
<div class="ui checkbox"> <div class="ui checkbox">
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label> <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>
</div> </div>
<button class="ui primary button"> <button class="ui primary button">

View 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);
});
}

View File

@ -78,6 +78,7 @@ import {initDirAuto} from './modules/dirauto.ts';
import {initRepositorySearch} from './features/repo-search.ts'; import {initRepositorySearch} from './features/repo-search.ts';
import {initColorPickers} from './features/colorpicker.ts'; import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
import {initGlobalFetchAction} from './features/common-fetch-action.ts'; import {initGlobalFetchAction} from './features/common-fetch-action.ts';
import { import {
initFootLanguageMenu, initFootLanguageMenu,
@ -225,5 +226,7 @@ onDomReady(() => {
initPdfViewer, initPdfViewer,
initScopedAccessTokenCategories, initScopedAccessTokenCategories,
initColorPickers, initColorPickers,
initOAuth2SettingsDisableCheckbox,
]); ]);
}); });