From e45849f039b20c3e06cee0b4bd34fcb3995c52ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 20 Mar 2024 12:31:33 -0400 Subject: [PATCH 1/5] incusd/cluster/config: Add oidc.claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #549 Signed-off-by: Stéphane Graber --- internal/server/cluster/config/config.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/server/cluster/config/config.go b/internal/server/cluster/config/config.go index 3bbedd0ab8a..951d37140a0 100644 --- a/internal/server/cluster/config/config.go +++ b/internal/server/cluster/config/config.go @@ -214,8 +214,8 @@ func (c *Config) RemoteTokenExpiry() string { } // OIDCServer returns all the OpenID Connect settings needed to connect to a server. -func (c *Config) OIDCServer() (string, string, string) { - return c.m.GetString("oidc.issuer"), c.m.GetString("oidc.client.id"), c.m.GetString("oidc.audience") +func (c *Config) OIDCServer() (string, string, string, string) { + return c.m.GetString("oidc.issuer"), c.m.GetString("oidc.client.id"), c.m.GetString("oidc.audience"), c.m.GetString("oidc.claim") } // ClusterHealingThreshold returns the configured healing threshold, i.e. the @@ -685,6 +685,14 @@ var ConfigSchema = config.Schema{ // shortdesc: Expected audience value for the application "oidc.audience": {}, + // gendoc:generate(entity=server, group=oidc, key=oidc.claim) + // + // --- + // type: string + // scope: global + // shortdesc: OpenID Connect claim to use as the username + "oidc.claim": {}, + // OVN networking global keys. // gendoc:generate(entity=server, group=miscellaneous, key=network.ovn.integration_bridge) From 0c2740da3d5469fdd198513a1a3ed98f561f618b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 20 Mar 2024 12:26:17 -0400 Subject: [PATCH 2/5] incusd/auth/oidc: Add support for using a specific claim as username MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- internal/server/auth/oidc/oidc.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/server/auth/oidc/oidc.go b/internal/server/auth/oidc/oidc.go index 9a6cf50022d..4324c5a48ab 100644 --- a/internal/server/auth/oidc/oidc.go +++ b/internal/server/auth/oidc/oidc.go @@ -23,6 +23,7 @@ type Verifier struct { clientID string issuer string audience string + claim string cookieKey []byte } @@ -129,6 +130,16 @@ func (o *Verifier) Auth(ctx context.Context, w http.ResponseWriter, r *http.Requ } } + if o.claim != "" { + claim := claims.Claims[o.claim] + username, ok := claim.(string) + if claim == nil || !ok || username == "" { + return "", fmt.Errorf("OIDC user is missing required claim %q", o.claim) + } + + return username, nil + } + user, ok := claims.Claims["email"] if ok && user != nil && user.(string) != "" { return user.(string), nil @@ -295,13 +306,13 @@ func getAccessTokenVerifier(issuer string) (op.AccessTokenVerifier, error) { } // NewVerifier returns a Verifier. -func NewVerifier(issuer string, clientid string, audience string) (*Verifier, error) { +func NewVerifier(issuer string, clientid string, audience string, claim string) (*Verifier, error) { cookieKey, err := uuid.New().MarshalBinary() if err != nil { return nil, fmt.Errorf("Failed to create UUID: %w", err) } - verifier := &Verifier{issuer: issuer, clientID: clientid, audience: audience, cookieKey: cookieKey} + verifier := &Verifier{issuer: issuer, clientID: clientid, audience: audience, cookieKey: cookieKey, claim: claim} verifier.accessTokenVerifier, _ = getAccessTokenVerifier(issuer) return verifier, nil From 82842d1f7c2d47b543683cc5e8890040415dd0d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 20 Mar 2024 12:29:20 -0400 Subject: [PATCH 3/5] incusd: Pass OIDC claim to verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- cmd/incusd/api_1.0.go | 6 +++--- cmd/incusd/daemon.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/incusd/api_1.0.go b/cmd/incusd/api_1.0.go index e460ae98207..1fc71947d54 100644 --- a/cmd/incusd/api_1.0.go +++ b/cmd/incusd/api_1.0.go @@ -213,7 +213,7 @@ func api10Get(d *Daemon, r *http.Request) response.Response { // Get the authentication methods. authMethods := []string{api.AuthenticationMethodTLS} - oidcIssuer, oidcClientID, _ := s.GlobalConfig.OIDCServer() + oidcIssuer, oidcClientID, _, _ := s.GlobalConfig.OIDCServer() if oidcIssuer != "" && oidcClientID != "" { authMethods = append(authMethods, api.AuthenticationMethodOIDC) } @@ -998,13 +998,13 @@ func doApi10UpdateTriggers(d *Daemon, nodeChanged, clusterChanged map[string]str } if oidcChanged { - oidcIssuer, oidcClientID, oidcAudience := clusterConfig.OIDCServer() + oidcIssuer, oidcClientID, oidcAudience, oidcClaim := clusterConfig.OIDCServer() if oidcIssuer == "" || oidcClientID == "" { d.oidcVerifier = nil } else { var err error - d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience) + d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience, oidcClaim) if err != nil { return fmt.Errorf("Failed creating verifier: %w", err) } diff --git a/cmd/incusd/daemon.go b/cmd/incusd/daemon.go index f947360fc0e..a3dd8410984 100644 --- a/cmd/incusd/daemon.go +++ b/cmd/incusd/daemon.go @@ -1380,7 +1380,7 @@ func (d *Daemon) init() error { d.gateway.HeartbeatOfflineThreshold = d.globalConfig.OfflineThreshold() lokiURL, lokiUsername, lokiPassword, lokiCACert, lokiInstance, lokiLoglevel, lokiLabels, lokiTypes := d.globalConfig.LokiServer() - oidcIssuer, oidcClientID, oidcAudience := d.globalConfig.OIDCServer() + oidcIssuer, oidcClientID, oidcAudience, oidcClaim := d.globalConfig.OIDCServer() syslogSocketEnabled := d.localConfig.SyslogSocket() openfgaAPIURL, openfgaAPIToken, openfgaStoreID, openFGAAuthorizationModelID := d.globalConfig.OpenFGA() instancePlacementScriptlet := d.globalConfig.InstancesPlacementScriptlet() @@ -1406,7 +1406,7 @@ func (d *Daemon) init() error { // Setup OIDC authentication. if oidcIssuer != "" && oidcClientID != "" { - d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience) + d.oidcVerifier, err = oidc.NewVerifier(oidcIssuer, oidcClientID, oidcAudience, oidcClaim) if err != nil { return err } From 5a13d25414b9e3e5ba751d8e0bd900c8f8f8f387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 20 Mar 2024 12:40:27 -0400 Subject: [PATCH 4/5] api: oidc_claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- doc/api-extensions.md | 4 ++++ internal/version/api.go | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/api-extensions.md b/doc/api-extensions.md index 9143cd87fe5..f09ecb4794e 100644 --- a/doc/api-extensions.md +++ b/doc/api-extensions.md @@ -2383,3 +2383,7 @@ This adds the ability to use a signed `JSON Web Token` (`JWT`) instead of using In this scenario, the client derives a `JWT` from their own TLS client certificate providing it as a `bearer` token through the `Authorization` HTTP header. The `JWT` must have the certificate's fingerprint as its `Subject` and must be signed by the client's private key. + +## `oidc_claim` + +This introduces a new `oidc.claim` server configuration key which can be used to specify what OpenID Connect claim to use as the username. diff --git a/internal/version/api.go b/internal/version/api.go index 5b1800cf51a..1b98917a24e 100644 --- a/internal/version/api.go +++ b/internal/version/api.go @@ -401,6 +401,7 @@ var APIExtensions = []string{ "storage_lvm_cluster", "shared_custom_block_volumes", "auth_tls_jwt", + "oidc_claim", } // APIExtensionsCount returns the number of available API extensions. From a5fb628bb793a09eba19714efd8f6b15ed49d52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Wed, 20 Mar 2024 12:41:22 -0400 Subject: [PATCH 5/5] doc: Update configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- doc/config_options.txt | 7 +++++++ internal/server/metadata/configuration.json | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/doc/config_options.txt b/doc/config_options.txt index 9cbb1e72271..55e0bbadcd4 100644 --- a/doc/config_options.txt +++ b/doc/config_options.txt @@ -1787,6 +1787,13 @@ Specify the volume using the syntax `POOL/VOLUME`. This value is required by some providers. ``` +```{config:option} oidc.claim server-oidc +:scope: "global" +:shortdesc: "OpenID Connect claim to use as the username" +:type: "string" + +``` + ```{config:option} oidc.client.id server-oidc :scope: "global" :shortdesc: "OpenID Connect client ID" diff --git a/internal/server/metadata/configuration.json b/internal/server/metadata/configuration.json index c7589b1cfb0..dbfffe1c874 100644 --- a/internal/server/metadata/configuration.json +++ b/internal/server/metadata/configuration.json @@ -1962,6 +1962,14 @@ "type": "string" } }, + { + "oidc.claim": { + "longdesc": "", + "scope": "global", + "shortdesc": "OpenID Connect claim to use as the username", + "type": "string" + } + }, { "oidc.client.id": { "longdesc": "",