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
10 changed files with 131 additions and 12 deletions
|
@ -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…
Reference in a new issue