Skip to content

Pre-register OAuth2 applications for git credential helpers #26291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
97d30ec
preconfigure git-credential-oauth
hickford Jul 3, 2023
798a5ec
Merge branch 'main' into git-credential-oauth
denyskon Jul 31, 2023
9e1fbee
Merge remote-tracking branch 'upstream/main' into git-credential-oauth
denyskon Aug 2, 2023
ad62ed5
refactor pre-registered application support
denyskon Aug 2, 2023
80a83b9
update app.example.ini
denyskon Aug 2, 2023
3139825
fmt docs
denyskon Aug 2, 2023
c1ca86c
lookup registered iauth2 apps by client id instead of user
denyskon Aug 2, 2023
b571702
Merge remote-tracking branch 'upstream/main' into git-credential-oauth
denyskon Aug 3, 2023
e6c3192
reference migration in migrations.go
denyskon Aug 3, 2023
b574494
prevent locked oauth2 apps from being deleted
denyskon Aug 3, 2023
950013d
Merge branch 'main' into git-credential-oauth
denyskon Aug 3, 2023
3c7f4bf
Merge branch 'main' into git-credential-oauth
denyskon Aug 4, 2023
f1bd5a3
refactor
wxiaoguang Aug 4, 2023
c9e9c0e
Merge pull request #3 from wxiaoguang/git-credential-oauth
denyskon Aug 5, 2023
2eb3b88
Merge remote-tracking branch 'upstream/main' into git-credential-oauth
denyskon Aug 8, 2023
844f55c
disallow editing builtin oauth2 applications
denyskon Aug 8, 2023
8aa3846
add oauth2 application description & client ID to docs
denyskon Aug 8, 2023
e5722d8
Merge remote-tracking branch 'upstream/main' into git-credential-oauth
denyskon Aug 8, 2023
3d101e3
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 8, 2023
9a29372
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 8, 2023
006e659
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 9, 2023
e12d8a6
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 9, 2023
bd337c4
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 9, 2023
d100268
Merge branch 'main' into git-credential-oauth
GiteaBot Aug 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
|     **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:

- [git-credential-oauth](https://github.com/hickford/git-credential-oauth)
- [Git Credential Manager](https://github.com/git-ecosystem/git-credential-manager)

as we assume that these are universally useful.

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 won't be visible if you revisit this page
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 to access 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've granted access to your personal Gitea account to these third party applications. Please revoke access for applications no longer needed.
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