Skip to content

Commit

Permalink
Pre-register OAuth2 applications for git credential helpers (#26291)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
3 people authored Aug 9, 2023
1 parent d41aee1 commit 63ab92d
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 12 deletions.
5 changes: 5 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
1 change: 1 addition & 0 deletions docs/content/administration/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
11 changes: 11 additions & 0 deletions docs/content/development/oauth2-provider.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ Gitea token scopes are as follows:
| &nbsp;&nbsp;&nbsp; **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. |
| &nbsp;&nbsp;&nbsp; **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).
Expand Down
91 changes: 91 additions & 0 deletions models/auth/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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(&registeredApps); 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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions modules/setting/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ edit = Edit

enabled = Enabled
disabled = Disabled
locked = Locked

copy = Copy
copy_url = Copy URL
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions routers/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion routers/web/admin/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Applications(ctx *context.Context) {
return
}
ctx.Data["Applications"] = apps

ctx.Data["BuiltinApplications"] = auth.BuiltinApplications()
ctx.HTML(http.StatusOK, tplSettingsApplications)
}

Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
25 changes: 15 additions & 10 deletions templates/user/settings/applications_oauth2_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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}}
Expand Down

0 comments on commit 63ab92d

Please sign in to comment.