Skip to content

Commit 3e93151

Browse files
committed
oidcccl,provisioning: add user provisioning for OIDC authentication
Previously, the OIDC authentication flow in the DB Console only supported logging in with existing user accounts. There was no built-in mechanism to automatically create a new database user when a user authenticated via an OIDC provider for the first time. This was inadequate because administrators would need to manually create a database user for every first‑time OIDC login, adding friction and overhead to onboarding. To address this, this patch introduces automatic user provisioning for the OIDC authentication flow. When a new user successfully authenticates via an OIDC provider, a corresponding CockroachDB user is now automatically created if one does not already exist. This functionality is controlled by a new cluster setting, `security.provisioning.oidc.enabled`, which is disabled by default to maintain backward compatibility and ensure administrators can opt-in to this behavior. Note: The `security.provisioning.oidc.enabled` cluster setting requires checking user existence before provisioning. This may introduce latency when concurrent OIDC authentication attempts from browsers generate high read request load on the user table. Fixes: #126680 Epic: CRDB-48764 Release note (enterprise change): A new cluster setting, `security.provisioning.oidc.enabled`, has been added to allow for the automatic provisioning of users when they log in for the first time via OIDC. When enabled, a new user will be created in CockroachDB upon their first successful OIDC authentication. This feature is disabled by default. On enabling the setting, user gets created on oidc login and can be validated using the `SHOW users` command. ``` > SELECT * FROM [SHOW USERS] WHERE username = 'testuser'; username | options | member_of | estimated_last_login_time -----------------+-------------------------------------------------+-----------+---------------------------- testuser | {PROVISIONSRC=oidc:https://accounts.google.com} | {} | NULL (1 row) NOTICE: estimated_last_login_time is computed on a best effort basis; it is not guaranteed to capture every login event ```
1 parent ab11edd commit 3e93151

File tree

7 files changed

+633
-11
lines changed

7 files changed

+633
-11
lines changed

pkg/ccl/logictestccl/testdata/logic_test/provisioning

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
statement error role "root" cannot have a PROVISIONSRC
44
ALTER ROLE root PROVISIONSRC 'ldap:ldap.example.com'
55

6-
statement error pq: PROVISIONSRC "ldap.example.com" was not prefixed with any valid auth methods \["ldap" "jwt_token"\]
6+
statement error pq: PROVISIONSRC "ldap.example.com" was not prefixed with any valid auth methods \["ldap" "jwt_token" "oidc"\]
77
CREATE ROLE role_with_provisioning PROVISIONSRC 'ldap.example.com'
88

99
statement error pq: conflicting role options

pkg/ccl/oidcccl/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ go_library(
1616
"//pkg/ccl/securityccl/jwthelper",
1717
"//pkg/ccl/utilccl",
1818
"//pkg/roachpb",
19+
"//pkg/security/provisioning",
1920
"//pkg/security/username",
2021
"//pkg/server",
2122
"//pkg/server/authserver",
@@ -55,6 +56,7 @@ go_test(
5556
"//pkg/ccl",
5657
"//pkg/roachpb",
5758
"//pkg/security/certnames",
59+
"//pkg/security/provisioning",
5860
"//pkg/security/securityassets",
5961
"//pkg/security/securitytest",
6062
"//pkg/security/username",

pkg/ccl/oidcccl/authentication_oidc.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/cockroachdb/cockroach/pkg/ccl/jwtauthccl"
2020
"github.com/cockroachdb/cockroach/pkg/ccl/utilccl"
2121
"github.com/cockroachdb/cockroach/pkg/roachpb"
22+
"github.com/cockroachdb/cockroach/pkg/security/provisioning"
2223
secuser "github.com/cockroachdb/cockroach/pkg/security/username"
2324
"github.com/cockroachdb/cockroach/pkg/server"
2425
"github.com/cockroachdb/cockroach/pkg/server/authserver"
@@ -44,6 +45,7 @@ const (
4445
codeKey = "code"
4546
stateKey = "state"
4647
secretCookieName = "oidc_secret"
48+
oidcProvisioningKey = "oidc"
4749
oidcLoginPath = "/oidc/v1/login"
4850
oidcCallbackPath = "/oidc/v1/callback"
4951
oidcJWTPath = "/oidc/v1/jwt"
@@ -158,6 +160,7 @@ type oidcAuthenticationConf struct {
158160
authZEnabled bool
159161
groupClaim string
160162
userinfoGroupKey string
163+
provisioningEnabled bool
161164
}
162165

163166
// GetOIDCConf is used to extract certain parts of the OIDC
@@ -420,9 +423,10 @@ func reloadConfigLocked(
420423
httputil.WithDialerTimeout(clientTimeout),
421424
httputil.WithCustomCAPEM(OIDCProviderCustomCA.Get(&st.SV)),
422425
),
423-
authZEnabled: OIDCAuthZEnabled.Get(&st.SV),
424-
groupClaim: OIDCAuthGroupClaim.Get(&st.SV),
425-
userinfoGroupKey: OIDCAuthUserinfoGroupKey.Get(&st.SV),
426+
authZEnabled: OIDCAuthZEnabled.Get(&st.SV),
427+
groupClaim: OIDCAuthGroupClaim.Get(&st.SV),
428+
userinfoGroupKey: OIDCAuthUserinfoGroupKey.Get(&st.SV),
429+
provisioningEnabled: provisioning.ClusterProvisioningConfig(st).Enabled("oidc"),
426430
}
427431

428432
if !oidcAuthServer.conf.enabled && conf.enabled {
@@ -490,6 +494,69 @@ func getRegionSpecificRedirectURL(locality roachpb.Locality, conf redirectURLCon
490494
return s, nil
491495
}
492496

497+
// maybeProvisionUserLocked checks the cached OIDC configuration to see whether
498+
// automatic user provisioning is enabled. If so, it attempts to create a SQL
499+
// user linked to the OIDC identity provider.
500+
//
501+
// This function is called after a successful OIDC token exchange and
502+
// verification. Its execution relies on the success of the underlying OIDC
503+
// library, which operates on the following assumptions:
504+
//
505+
// 1. OIDC Discovery: The library uses the OIDC discovery protocol to fetch the
506+
// provider's configuration from "/.well-known/openid-configuration". This
507+
// assumes the provider has discovery enabled and accessible.
508+
//
509+
// 2. Issuer Validation: The go-oidc library's verifier ensures the 'iss' claim
510+
// in the ID Token matches the issuer URL from the discovery document. There
511+
// is also an exception made to this in go-oidc for accounts.google.com
512+
//
513+
// Errors during username validation, provisioning source parsing, or user creation
514+
// are logged with detailed context and result in an HTTP 500 response with a
515+
// generic error message to the client.
516+
func maybeProvisionUserLocked(
517+
ctx context.Context,
518+
authConf oidcAuthenticationConf,
519+
execCfg *sql.ExecutorConfig,
520+
username string,
521+
w http.ResponseWriter,
522+
) (err error) {
523+
if !authConf.provisioningEnabled {
524+
return
525+
}
526+
527+
log.Dev.Infof(ctx, "OIDC: attempting user provisioning for %s", username)
528+
telemetry.Inc(provisioning.BeginOIDCProvisionUseCounter)
529+
530+
// Convert the extracted username string to a username.SQLUsername type.
531+
sqlUsername, err := secuser.MakeSQLUsernameFromUserInput(username, secuser.PurposeCreation)
532+
if err != nil {
533+
log.Dev.Errorf(ctx, "OIDC provisioning: invalid username format for %s: %v", username, err)
534+
http.Error(w, "OIDC: invalid username format", http.StatusInternalServerError)
535+
return err
536+
}
537+
538+
// Create the provisioning source identifier string, e.g., "oidc:https://accounts.example.com".
539+
idpString := oidcProvisioningKey + ":" + authConf.providerURL
540+
provisioningSource, err := provisioning.ParseProvisioningSource(idpString)
541+
if err != nil {
542+
// This error occurs if the provisioning package doesn't recognize the "oidc:" prefix.
543+
log.Dev.Errorf(ctx, "OIDC provisioning: error parsing provisioning source IDP %s: %v", idpString, err)
544+
http.Error(w, "OIDC: provisioning error", http.StatusInternalServerError)
545+
return err
546+
}
547+
548+
// Call the core provisioning function using the execCfg.
549+
if err = sql.CreateRoleForProvisioning(ctx, execCfg, sqlUsername, provisioningSource.String()); err != nil {
550+
log.Dev.Errorf(ctx, "OIDC provisioning: error provisioning user %s: %v", sqlUsername, err)
551+
http.Error(w, "OIDC: provisioning error", http.StatusInternalServerError)
552+
return err
553+
}
554+
555+
log.Dev.Infof(ctx, "OIDC: successfully provisioned user %s", sqlUsername)
556+
telemetry.Inc(provisioning.ProvisionOIDCSuccessCounter)
557+
return
558+
}
559+
493560
// ConfigureOIDC attaches handlers to the server `mux` that
494561
// can initiate and complete an OIDC authentication flow.
495562
// This flow consists of an initial login request that triggers
@@ -608,6 +675,12 @@ var ConfigureOIDC = func(
608675
return
609676
}
610677

678+
// OIDC user provisioning
679+
if err := maybeProvisionUserLocked(ctx, oidcAuthentication.conf, oidcAuthentication.execCfg, username, w); err != nil {
680+
log.Dev.Errorf(ctx, "OIDC provisioning failed with error: %v", err)
681+
return
682+
}
683+
611684
// OIDC authorization
612685
if err := oidcAuthentication.authorize(ctx, rawIDToken, rawAccessToken, username); err != nil {
613686
log.Dev.Errorf(ctx, "OIDC authorization failed with error: %v", err)
@@ -938,6 +1011,9 @@ var ConfigureOIDC = func(
9381011
OIDCAuthUserinfoGroupKey.SetOnChange(&st.SV, func(ctx context.Context) {
9391012
reloadConfig(ambientCtx.AnnotateCtx(ctx), oidcAuthentication, locality, st)
9401013
})
1014+
provisioning.OIDCProvisioningEnabled.SetOnChange(&st.SV, func(ctx context.Context) {
1015+
reloadConfig(ambientCtx.AnnotateCtx(ctx), oidcAuthentication, locality, st)
1016+
})
9411017

9421018
return oidcAuthentication, nil
9431019
}

0 commit comments

Comments
 (0)