mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add CODER_OIDC_IGNORE_EMAIL_VERIFIED config knob (#5165)
* Adds a configuration knob CODER_OIDC_IGNORE_EMAIL_VERIFIED that allows ignoring the email_verified OIDC claim * Adds warning message at startup if CODER_OIDC_IGNORE_EMAIL_VERIFIED=true * Adds warning whenever an unverified OIDC email is let through * Skips flaky test on non-linux platforms Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
This commit is contained in:
@ -231,6 +231,12 @@ func newConfig() *codersdk.DeploymentConfig {
|
|||||||
Flag: "oidc-scopes",
|
Flag: "oidc-scopes",
|
||||||
Default: []string{oidc.ScopeOpenID, "profile", "email"},
|
Default: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
},
|
},
|
||||||
|
IgnoreEmailVerified: &codersdk.DeploymentConfigField[bool]{
|
||||||
|
Name: "OIDC Ignore Email Verified",
|
||||||
|
Usage: "Ignore the email_verified claim from the upstream provider.",
|
||||||
|
Flag: "oidc-ignore-email-verified",
|
||||||
|
Default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Telemetry: &codersdk.TelemetryConfig{
|
Telemetry: &codersdk.TelemetryConfig{
|
||||||
|
@ -122,23 +122,37 @@ func TestConfig(t *testing.T) {
|
|||||||
require.Equal(t, config.Trace.Enable.Value, true)
|
require.Equal(t, config.Trace.Enable.Value, true)
|
||||||
require.Equal(t, config.Trace.HoneycombAPIKey.Value, "my-honeycomb-key")
|
require.Equal(t, config.Trace.HoneycombAPIKey.Value, "my-honeycomb-key")
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
Name: "OIDC_Defaults",
|
||||||
|
Env: map[string]string{},
|
||||||
|
Valid: func(config *codersdk.DeploymentConfig) {
|
||||||
|
require.Empty(t, config.OIDC.IssuerURL.Value)
|
||||||
|
require.Empty(t, config.OIDC.EmailDomain.Value)
|
||||||
|
require.Empty(t, config.OIDC.ClientID.Value)
|
||||||
|
require.Empty(t, config.OIDC.ClientSecret.Value)
|
||||||
|
require.True(t, config.OIDC.AllowSignups.Value)
|
||||||
|
require.ElementsMatch(t, config.OIDC.Scopes.Value, []string{"openid", "email", "profile"})
|
||||||
|
require.False(t, config.OIDC.IgnoreEmailVerified.Value)
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
Name: "OIDC",
|
Name: "OIDC",
|
||||||
Env: map[string]string{
|
Env: map[string]string{
|
||||||
"CODER_OIDC_ISSUER_URL": "https://accounts.google.com",
|
"CODER_OIDC_ISSUER_URL": "https://accounts.google.com",
|
||||||
"CODER_OIDC_EMAIL_DOMAIN": "coder.com",
|
"CODER_OIDC_EMAIL_DOMAIN": "coder.com",
|
||||||
"CODER_OIDC_CLIENT_ID": "client",
|
"CODER_OIDC_CLIENT_ID": "client",
|
||||||
"CODER_OIDC_CLIENT_SECRET": "secret",
|
"CODER_OIDC_CLIENT_SECRET": "secret",
|
||||||
"CODER_OIDC_ALLOW_SIGNUPS": "false",
|
"CODER_OIDC_ALLOW_SIGNUPS": "false",
|
||||||
"CODER_OIDC_SCOPES": "something,here",
|
"CODER_OIDC_SCOPES": "something,here",
|
||||||
|
"CODER_OIDC_IGNORE_EMAIL_VERIFIED": "true",
|
||||||
},
|
},
|
||||||
Valid: func(config *codersdk.DeploymentConfig) {
|
Valid: func(config *codersdk.DeploymentConfig) {
|
||||||
require.Equal(t, config.OIDC.IssuerURL.Value, "https://accounts.google.com")
|
require.Equal(t, config.OIDC.IssuerURL.Value, "https://accounts.google.com")
|
||||||
require.Equal(t, config.OIDC.EmailDomain.Value, "coder.com")
|
require.Equal(t, config.OIDC.EmailDomain.Value, "coder.com")
|
||||||
require.Equal(t, config.OIDC.ClientID.Value, "client")
|
require.Equal(t, config.OIDC.ClientID.Value, "client")
|
||||||
require.Equal(t, config.OIDC.ClientSecret.Value, "secret")
|
require.Equal(t, config.OIDC.ClientSecret.Value, "secret")
|
||||||
require.Equal(t, config.OIDC.AllowSignups.Value, false)
|
require.False(t, config.OIDC.AllowSignups.Value)
|
||||||
require.Equal(t, config.OIDC.Scopes.Value, []string{"something", "here"})
|
require.Equal(t, config.OIDC.Scopes.Value, []string{"something", "here"})
|
||||||
|
require.True(t, config.OIDC.IgnoreEmailVerified.Value)
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
Name: "GitHub",
|
Name: "GitHub",
|
||||||
|
@ -398,6 +398,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
|
|||||||
return xerrors.Errorf("configure oidc client certificates: %w", err)
|
return xerrors.Errorf("configure oidc client certificates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.OIDC.IgnoreEmailVerified.Value {
|
||||||
|
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
|
||||||
|
}
|
||||||
|
|
||||||
oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDC.IssuerURL.Value)
|
oidcProvider, err := oidc.NewProvider(ctx, cfg.OIDC.IssuerURL.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("configure oidc provider: %w", err)
|
return xerrors.Errorf("configure oidc provider: %w", err)
|
||||||
|
3
cli/testdata/coder_server_--help.golden
vendored
3
cli/testdata/coder_server_--help.golden
vendored
@ -98,6 +98,9 @@ Flags:
|
|||||||
--oidc-email-domain string Email domain that clients logging in with
|
--oidc-email-domain string Email domain that clients logging in with
|
||||||
OIDC must match.
|
OIDC must match.
|
||||||
Consumes $CODER_OIDC_EMAIL_DOMAIN
|
Consumes $CODER_OIDC_EMAIL_DOMAIN
|
||||||
|
--oidc-ignore-email-verified Ignore the email_verified claim from the
|
||||||
|
upstream provider.
|
||||||
|
Consumes $CODER_OIDC_IGNORE_EMAIL_VERIFIED
|
||||||
--oidc-issuer-url string Issuer URL to use for Login with OIDC.
|
--oidc-issuer-url string Issuer URL to use for Login with OIDC.
|
||||||
Consumes $CODER_OIDC_ISSUER_URL
|
Consumes $CODER_OIDC_ISSUER_URL
|
||||||
--oidc-scopes strings Scopes to grant when authenticating with
|
--oidc-scopes strings Scopes to grant when authenticating with
|
||||||
|
@ -195,6 +195,9 @@ type OIDCConfig struct {
|
|||||||
// EmailDomain is the domain to enforce when a user authenticates.
|
// EmailDomain is the domain to enforce when a user authenticates.
|
||||||
EmailDomain string
|
EmailDomain string
|
||||||
AllowSignups bool
|
AllowSignups bool
|
||||||
|
// IgnoreEmailVerified allows ignoring the email_verified claim
|
||||||
|
// from an upstream OIDC provider. See #5065 for context.
|
||||||
|
IgnoreEmailVerified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
||||||
@ -264,10 +267,13 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||||||
if ok {
|
if ok {
|
||||||
verified, ok := verifiedRaw.(bool)
|
verified, ok := verifiedRaw.(bool)
|
||||||
if ok && !verified {
|
if ok && !verified {
|
||||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
if !api.OIDCConfig.IgnoreEmailVerified {
|
||||||
Message: fmt.Sprintf("Verify the %q email address on your OIDC provider to authenticate!", email),
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||||
})
|
Message: fmt.Sprintf("Verify the %q email address on your OIDC provider to authenticate!", email),
|
||||||
return
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.Logger.Warn(ctx, "allowing unverified oidc email %q")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The username is a required property in Coder. We make a best-effort
|
// The username is a required property in Coder. We make a best-effort
|
||||||
|
@ -479,13 +479,14 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
Name string
|
Name string
|
||||||
Claims jwt.MapClaims
|
Claims jwt.MapClaims
|
||||||
AllowSignups bool
|
AllowSignups bool
|
||||||
EmailDomain string
|
EmailDomain string
|
||||||
Username string
|
Username string
|
||||||
AvatarURL string
|
AvatarURL string
|
||||||
StatusCode int
|
StatusCode int
|
||||||
|
IgnoreEmailVerified bool
|
||||||
}{{
|
}{{
|
||||||
Name: "EmailOnly",
|
Name: "EmailOnly",
|
||||||
Claims: jwt.MapClaims{
|
Claims: jwt.MapClaims{
|
||||||
@ -502,6 +503,24 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AllowSignups: true,
|
AllowSignups: true,
|
||||||
StatusCode: http.StatusForbidden,
|
StatusCode: http.StatusForbidden,
|
||||||
|
}, {
|
||||||
|
Name: "EmailNotAString",
|
||||||
|
Claims: jwt.MapClaims{
|
||||||
|
"email": 3.14159,
|
||||||
|
"email_verified": false,
|
||||||
|
},
|
||||||
|
AllowSignups: true,
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}, {
|
||||||
|
Name: "EmailNotVerifiedIgnored",
|
||||||
|
Claims: jwt.MapClaims{
|
||||||
|
"email": "kyle@kwc.io",
|
||||||
|
"email_verified": false,
|
||||||
|
},
|
||||||
|
AllowSignups: true,
|
||||||
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
|
Username: "kyle",
|
||||||
|
IgnoreEmailVerified: true,
|
||||||
}, {
|
}, {
|
||||||
Name: "NotInRequiredEmailDomain",
|
Name: "NotInRequiredEmailDomain",
|
||||||
Claims: jwt.MapClaims{
|
Claims: jwt.MapClaims{
|
||||||
@ -593,6 +612,7 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
config := conf.OIDCConfig()
|
config := conf.OIDCConfig()
|
||||||
config.AllowSignups = tc.AllowSignups
|
config.AllowSignups = tc.AllowSignups
|
||||||
config.EmailDomain = tc.EmailDomain
|
config.EmailDomain = tc.EmailDomain
|
||||||
|
config.IgnoreEmailVerified = tc.IgnoreEmailVerified
|
||||||
|
|
||||||
client := coderdtest.New(t, &coderdtest.Options{
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
OIDCConfig: config,
|
OIDCConfig: config,
|
||||||
|
@ -87,12 +87,13 @@ type OAuth2GithubConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OIDCConfig struct {
|
type OIDCConfig struct {
|
||||||
AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"`
|
AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"`
|
||||||
ClientID *DeploymentConfigField[string] `json:"client_id" typescript:",notnull"`
|
ClientID *DeploymentConfigField[string] `json:"client_id" typescript:",notnull"`
|
||||||
ClientSecret *DeploymentConfigField[string] `json:"client_secret" typescript:",notnull"`
|
ClientSecret *DeploymentConfigField[string] `json:"client_secret" typescript:",notnull"`
|
||||||
EmailDomain *DeploymentConfigField[string] `json:"email_domain" typescript:",notnull"`
|
EmailDomain *DeploymentConfigField[string] `json:"email_domain" typescript:",notnull"`
|
||||||
IssuerURL *DeploymentConfigField[string] `json:"issuer_url" typescript:",notnull"`
|
IssuerURL *DeploymentConfigField[string] `json:"issuer_url" typescript:",notnull"`
|
||||||
Scopes *DeploymentConfigField[[]string] `json:"scopes" typescript:",notnull"`
|
Scopes *DeploymentConfigField[[]string] `json:"scopes" typescript:",notnull"`
|
||||||
|
IgnoreEmailVerified *DeploymentConfigField[bool] `json:"ignore_email_verified" typescript:",notnull"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelemetryConfig struct {
|
type TelemetryConfig struct {
|
||||||
|
@ -76,11 +76,21 @@ Once complete, run `sudo service coder restart` to reboot Coder.
|
|||||||
> When a new user is created, the `preferred_username` claim becomes the username. If this claim is empty, the email address will be stripped of the domain, and become the username (e.g. `example@coder.com` becomes `example`).
|
> When a new user is created, the `preferred_username` claim becomes the username. If this claim is empty, the email address will be stripped of the domain, and become the username (e.g. `example@coder.com` becomes `example`).
|
||||||
|
|
||||||
If your OpenID Connect provider requires client TLS certificates for authentication, you can configure them like so:
|
If your OpenID Connect provider requires client TLS certificates for authentication, you can configure them like so:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
CODER_TLS_CLIENT_CERT_FILE=/path/to/cert.pem
|
CODER_TLS_CLIENT_CERT_FILE=/path/to/cert.pem
|
||||||
CODER_TLS_CLIENT_KEY_FILE=/path/to/key.pem
|
CODER_TLS_CLIENT_KEY_FILE=/path/to/key.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Coder requires all OIDC email addresses to be verified by default. If the `email_verified` claim is present in the token response from the identity provider, Coder will validate that its value is `true`.
|
||||||
|
If needed, you can disable this behavior with the following setting:
|
||||||
|
|
||||||
|
```console
|
||||||
|
CODER_OIDC_IGNORE_EMAIL_VERIFIED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** This will cause Coder to implicitly treat all OIDC emails as "verified".
|
||||||
|
|
||||||
## SCIM (enterprise)
|
## SCIM (enterprise)
|
||||||
|
|
||||||
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
|
Coder supports user provisioning and deprovisioning via SCIM 2.0 with header
|
||||||
|
@ -23,8 +23,8 @@ import (
|
|||||||
|
|
||||||
func Test_Runner(t *testing.T) {
|
func Test_Runner(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS != "linux" {
|
||||||
t.Skip("PTY is flakey on Windows")
|
t.Skip("PTY is flakey on non-Linux platforms")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("OK", func(t *testing.T) {
|
t.Run("OK", func(t *testing.T) {
|
||||||
|
@ -441,6 +441,7 @@ export interface OIDCConfig {
|
|||||||
readonly email_domain: DeploymentConfigField<string>
|
readonly email_domain: DeploymentConfigField<string>
|
||||||
readonly issuer_url: DeploymentConfigField<string>
|
readonly issuer_url: DeploymentConfigField<string>
|
||||||
readonly scopes: DeploymentConfigField<string[]>
|
readonly scopes: DeploymentConfigField<string[]>
|
||||||
|
readonly ignore_email_verified: DeploymentConfigField<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/organizations.go
|
// From codersdk/organizations.go
|
||||||
|
Reference in New Issue
Block a user