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 <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,7 +15,11 @@
|
||||||
<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">
|
||||||
|
{{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">
|
<a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button">
|
||||||
{{svg "octicon-pencil" 16 "gt-mr-2"}}
|
{{svg "octicon-pencil" 16 "gt-mr-2"}}
|
||||||
{{$.locale.Tr "settings.oauth2_application_edit"}}
|
{{$.locale.Tr "settings.oauth2_application_edit"}}
|
||||||
|
@ -25,6 +29,7 @@
|
||||||
{{svg "octicon-trash" 16 "gt-mr-2"}}
|
{{svg "octicon-trash" 16 "gt-mr-2"}}
|
||||||
{{$.locale.Tr "settings.delete_key"}}
|
{{$.locale.Tr "settings.delete_key"}}
|
||||||
</button>
|
</button>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in a new issue