summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxim Slipenko <maxim@slipenko.com>2024-12-09 16:59:11 +0100
committerMaxim Slipenko <maxim@slipenko.com>2024-12-09 16:59:11 +0100
commit4500757acd3fb3c5f4fea94ee71247eedc4013d6 (patch)
tree6766addd12f10e36d02ab73c16201ac4993ab8c7
parentMerge pull request 'Feat: Add support for `pacman -F` in Arch package' (#6180... (diff)
downloadforgejo-4500757acd3fb3c5f4fea94ee71247eedc4013d6.tar.xz
forgejo-4500757acd3fb3c5f4fea94ee71247eedc4013d6.zip
feat: add synchronization for SSH keys with OpenID Connect
Co-authored-by: Kirill Kolmykov <cyberk1ra@ya.ru>
-rw-r--r--routers/web/admin/auths.go1
-rw-r--r--services/auth/source/oauth2/providers_base.go4
-rw-r--r--services/auth/source/oauth2/providers_openid.go4
-rw-r--r--services/auth/source/oauth2/source.go26
-rw-r--r--services/auth/source/oauth2/source_sync.go92
-rw-r--r--services/forms/auth_form.go1
-rw-r--r--templates/admin/auth/edit.tmpl25
-rw-r--r--templates/admin/auth/source/oauth.tmpl26
-rw-r--r--web_src/js/features/admin/common.js6
9 files changed, 158 insertions, 27 deletions
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 799b7e8a84..dcdc8e6a2a 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -197,6 +197,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
CustomURLMapping: customURLMapping,
IconURL: form.Oauth2IconURL,
Scopes: scopes,
+ AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey,
RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue,
SkipLocalTwoFA: form.SkipLocalTwoFA,
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index 9d4ab106e5..63318b84ef 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -48,4 +48,8 @@ func (b *BaseProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
+func (b *BaseProvider) CanProvideSSHKeys() bool {
+ return false
+}
+
var _ Provider = &BaseProvider{}
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index 285876d5ac..f606581271 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -51,6 +51,10 @@ func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings {
return nil
}
+func (o *OpenIDProvider) CanProvideSSHKeys() bool {
+ return true
+}
+
var _ GothProvider = &OpenIDProvider{}
func init() {
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 3454c9ad55..fe4823e778 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -4,6 +4,8 @@
package oauth2
import (
+ "strings"
+
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/json"
)
@@ -17,15 +19,16 @@ type Source struct {
CustomURLMapping *CustomURLMapping
IconURL string
- Scopes []string
- RequiredClaimName string
- RequiredClaimValue string
- GroupClaimName string
- AdminGroup string
- GroupTeamMap string
- GroupTeamMapRemoval bool
- RestrictedGroup string
- SkipLocalTwoFA bool `json:",omitempty"`
+ Scopes []string
+ AttributeSSHPublicKey string
+ RequiredClaimName string
+ RequiredClaimValue string
+ GroupClaimName string
+ AdminGroup string
+ GroupTeamMap string
+ GroupTeamMapRemoval bool
+ RestrictedGroup string
+ SkipLocalTwoFA bool `json:",omitempty"`
// reference to the authSource
authSource *auth.Source
@@ -41,6 +44,11 @@ func (source *Source) ToDB() ([]byte, error) {
return json.Marshal(source)
}
+// ProvidesSSHKeys returns if this source provides SSH Keys
+func (source *Source) ProvidesSSHKeys() bool {
+ return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+}
+
// SetAuthSource sets the related AuthSource
func (source *Source) SetAuthSource(authSource *auth.Source) {
source.authSource = authSource
diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go
index 5e30313c8f..667c0957fc 100644
--- a/services/auth/source/oauth2/source_sync.go
+++ b/services/auth/source/oauth2/source_sync.go
@@ -5,15 +5,20 @@ package oauth2
import (
"context"
+ "fmt"
"time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
"github.com/markbates/goth"
+ "github.com/markbates/goth/providers/openidConnect"
"golang.org/x/oauth2"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
)
// Sync causes this OAuth2 source to synchronize its users with the db.
@@ -108,7 +113,94 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us
u.RefreshToken = token.RefreshToken
}
+ needUserFetch := source.ProvidesSSHKeys()
+
+ if needUserFetch {
+ fetchedUser, err := fetchUser(provider, token)
+ if err != nil {
+ log.Error("fetchUser: %v", err)
+ } else {
+ err = updateSSHKeys(ctx, source, user, &fetchedUser)
+ if err != nil {
+ log.Error("updateSshKeys: %v", err)
+ }
+ }
+ }
+
err = user_model.UpdateExternalUserByExternalID(ctx, u)
return err
}
+
+func fetchUser(provider goth.Provider, token *oauth2.Token) (goth.User, error) {
+ state, err := util.CryptoRandomString(40)
+ if err != nil {
+ return goth.User{}, err
+ }
+
+ session, err := provider.BeginAuth(state)
+ if err != nil {
+ return goth.User{}, err
+ }
+
+ if s, ok := session.(*openidConnect.Session); ok {
+ s.AccessToken = token.AccessToken
+ s.RefreshToken = token.RefreshToken
+ s.ExpiresAt = token.Expiry
+ s.IDToken = token.Extra("id_token").(string)
+ }
+
+ gothUser, err := provider.FetchUser(session)
+ if err != nil {
+ return goth.User{}, err
+ }
+
+ return gothUser, nil
+}
+
+func updateSSHKeys(
+ ctx context.Context,
+ source *Source,
+ user *user_model.User,
+ fetchedUser *goth.User,
+) error {
+ if source.ProvidesSSHKeys() {
+ sshKeys, err := getSSHKeys(source, fetchedUser)
+ if err != nil {
+ return err
+ }
+
+ if asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sshKeys) {
+ err = asymkey_model.RewriteAllPublicKeys(ctx)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func getSSHKeys(source *Source, gothUser *goth.User) ([]string, error) {
+ key := source.AttributeSSHPublicKey
+ value, exists := gothUser.RawData[key]
+ if !exists {
+ return nil, fmt.Errorf("attribute '%s' not found in user data", key)
+ }
+
+ rawSlice, ok := value.([]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value)
+ }
+
+ sshKeys := make([]string, 0, len(rawSlice))
+ for i, v := range rawSlice {
+ str, ok := v.(string)
+ if !ok {
+ return nil, fmt.Errorf("unexpected element type at index %d in SSH public key array, expected string but got %T", i, v)
+ }
+ sshKeys = append(sshKeys, str)
+ }
+
+ return sshKeys, nil
+}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index f0da63155a..39aae51756 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -75,6 +75,7 @@ type AuthenticationForm struct {
Oauth2RestrictedGroup string
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
Oauth2GroupTeamMapRemoval bool
+ Oauth2AttributeSSHPublicKey string
SkipLocalTwoFA bool
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index a8b2049f92..84fefc0484 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -326,19 +326,28 @@
<input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
</div>
- {{range .OAuth2Providers}}{{if .CustomURLSettings}}
- <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
- <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
- <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
- <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
- <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
- <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
- {{end}}{{end}}
+ {{range .OAuth2Providers}}
+ {{if .CustomURLSettings}}
+ <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
+ <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
+ <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
+ <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
+ <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
+ <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
+ {{end}}
+ {{if .CanProvideSSHKeys}}
+ <input id="{{.Name}}_canProvideSSHKeys" type="hidden">
+ {{end}}
+ {{end}}
<div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
</div>
+ <div class="oauth2_attribute_ssh_public_key field">
+ <label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label>
+ <input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{$cfg.AttributeSSHPublicKey}}" placeholder="sshpubkey">
+ </div>
<div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 0560cc8256..7d0a64d269 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -63,19 +63,27 @@
<input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
</div>
- {{range .OAuth2Providers}}{{if .CustomURLSettings}}
- <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
- <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
- <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
- <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
- <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
- <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
- {{end}}{{end}}
-
+ {{range .OAuth2Providers}}
+ {{if .CustomURLSettings}}
+ <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
+ <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
+ <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
+ <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
+ <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
+ <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
+ {{end}}
+ {{if .CanProvideSSHKeys}}
+ <input id="{{.Name}}_canProvideSSHKeys" type="hidden">
+ {{end}}
+ {{end}}
<div class="field">
<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
</div>
+ <div class="oauth2_attribute_ssh_public_key field">
+ <label for="oauth2_attribute_ssh_public_key">{{ctx.Locale.Tr "admin.auths.attribute_ssh_public_key"}}</label>
+ <input id="oauth2_attribute_ssh_public_key" name="oauth2_attribute_ssh_public_key" value="{{.attribute_ssh_public_key}}" placeholder="sshpubkey">
+ </div>
<div class="field">
<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js
index 1a5bd6e490..a42d8261f1 100644
--- a/web_src/js/features/admin/common.js
+++ b/web_src/js/features/admin/common.js
@@ -62,7 +62,7 @@ export function initAdminCommon() {
}
function onOAuth2Change(applyDefaultValues) {
- hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url');
+ hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url, .oauth2_attribute_ssh_public_key');
for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) {
input.removeAttribute('required');
}
@@ -85,6 +85,10 @@ export function initAdminCommon() {
}
}
}
+ const canProvideSSHKeys = document.getElementById(`${provider}_canProvideSSHKeys`);
+ if (canProvideSSHKeys) {
+ showElem('.oauth2_attribute_ssh_public_key');
+ }
onOAuth2UseCustomURLChange(applyDefaultValues);
}