mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd)!: add CODER_OIDC_IGNORE_USERINFO configuration option (#6922)
* add CODER_OIDC_IGNORE_USERINFO option * chore: update docs for CODER_OIDC_IGNORE_USERINFO w.r.t ADFS * fix!: codersdk: fix incorrectly named OIDC_GROUP_MAPPING -> CODER_OIDC_GROUP_MAPPING
This commit is contained in:
@@ -731,6 +731,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
|||||||
UsernameField: cfg.OIDC.UsernameField.String(),
|
UsernameField: cfg.OIDC.UsernameField.String(),
|
||||||
EmailField: cfg.OIDC.EmailField.String(),
|
EmailField: cfg.OIDC.EmailField.String(),
|
||||||
AuthURLParams: cfg.OIDC.AuthURLParams.Value,
|
AuthURLParams: cfg.OIDC.AuthURLParams.Value,
|
||||||
|
IgnoreUserInfo: cfg.OIDC.IgnoreUserInfo.Value(),
|
||||||
GroupField: cfg.OIDC.GroupField.String(),
|
GroupField: cfg.OIDC.GroupField.String(),
|
||||||
GroupMapping: cfg.OIDC.GroupMapping.Value,
|
GroupMapping: cfg.OIDC.GroupMapping.Value,
|
||||||
SignInText: cfg.OIDC.SignInText.String(),
|
SignInText: cfg.OIDC.SignInText.String(),
|
||||||
|
|||||||
@@ -1086,6 +1086,7 @@ func TestServer(t *testing.T) {
|
|||||||
require.Equal(t, "preferred_username", deploymentConfig.Values.OIDC.UsernameField.Value())
|
require.Equal(t, "preferred_username", deploymentConfig.Values.OIDC.UsernameField.Value())
|
||||||
require.Equal(t, "email", deploymentConfig.Values.OIDC.EmailField.Value())
|
require.Equal(t, "email", deploymentConfig.Values.OIDC.EmailField.Value())
|
||||||
require.Equal(t, map[string]string{"access_type": "offline"}, deploymentConfig.Values.OIDC.AuthURLParams.Value)
|
require.Equal(t, map[string]string{"access_type": "offline"}, deploymentConfig.Values.OIDC.AuthURLParams.Value)
|
||||||
|
require.False(t, deploymentConfig.Values.OIDC.IgnoreUserInfo.Value())
|
||||||
require.Empty(t, deploymentConfig.Values.OIDC.GroupField.Value())
|
require.Empty(t, deploymentConfig.Values.OIDC.GroupField.Value())
|
||||||
require.Empty(t, deploymentConfig.Values.OIDC.GroupMapping.Value)
|
require.Empty(t, deploymentConfig.Values.OIDC.GroupMapping.Value)
|
||||||
require.Equal(t, "OpenID Connect", deploymentConfig.Values.OIDC.SignInText.Value())
|
require.Equal(t, "OpenID Connect", deploymentConfig.Values.OIDC.SignInText.Value())
|
||||||
@@ -1125,6 +1126,7 @@ func TestServer(t *testing.T) {
|
|||||||
"--oidc-username-field", "not_preferred_username",
|
"--oidc-username-field", "not_preferred_username",
|
||||||
"--oidc-email-field", "not_email",
|
"--oidc-email-field", "not_email",
|
||||||
"--oidc-auth-url-params", `{"prompt":"consent"}`,
|
"--oidc-auth-url-params", `{"prompt":"consent"}`,
|
||||||
|
"--oidc-ignore-userinfo",
|
||||||
"--oidc-group-field", "serious_business_unit",
|
"--oidc-group-field", "serious_business_unit",
|
||||||
"--oidc-group-mapping", `{"serious_business_unit": "serious_business_unit"}`,
|
"--oidc-group-mapping", `{"serious_business_unit": "serious_business_unit"}`,
|
||||||
"--oidc-sign-in-text", "Sign In With Coder",
|
"--oidc-sign-in-text", "Sign In With Coder",
|
||||||
@@ -1169,6 +1171,7 @@ func TestServer(t *testing.T) {
|
|||||||
require.True(t, deploymentConfig.Values.OIDC.IgnoreEmailVerified.Value())
|
require.True(t, deploymentConfig.Values.OIDC.IgnoreEmailVerified.Value())
|
||||||
require.Equal(t, "not_preferred_username", deploymentConfig.Values.OIDC.UsernameField.Value())
|
require.Equal(t, "not_preferred_username", deploymentConfig.Values.OIDC.UsernameField.Value())
|
||||||
require.Equal(t, "not_email", deploymentConfig.Values.OIDC.EmailField.Value())
|
require.Equal(t, "not_email", deploymentConfig.Values.OIDC.EmailField.Value())
|
||||||
|
require.True(t, deploymentConfig.Values.OIDC.IgnoreUserInfo.Value())
|
||||||
require.Equal(t, map[string]string{"prompt": "consent"}, deploymentConfig.Values.OIDC.AuthURLParams.Value)
|
require.Equal(t, map[string]string{"prompt": "consent"}, deploymentConfig.Values.OIDC.AuthURLParams.Value)
|
||||||
require.Equal(t, "serious_business_unit", deploymentConfig.Values.OIDC.GroupField.Value())
|
require.Equal(t, "serious_business_unit", deploymentConfig.Values.OIDC.GroupField.Value())
|
||||||
require.Equal(t, map[string]string{"serious_business_unit": "serious_business_unit"}, deploymentConfig.Values.OIDC.GroupMapping.Value)
|
require.Equal(t, map[string]string{"serious_business_unit": "serious_business_unit"}, deploymentConfig.Values.OIDC.GroupMapping.Value)
|
||||||
|
|||||||
+5
-1
@@ -280,13 +280,17 @@ can safely ignore these settings.
|
|||||||
Change the OIDC default 'groups' claim field. By default, will be
|
Change the OIDC default 'groups' claim field. By default, will be
|
||||||
'groups' if present in the oidc scopes argument.
|
'groups' if present in the oidc scopes argument.
|
||||||
|
|
||||||
--oidc-group-mapping struct[map[string]string], $OIDC_GROUP_MAPPING (default: {})
|
--oidc-group-mapping struct[map[string]string], $CODER_OIDC_GROUP_MAPPING (default: {})
|
||||||
A map of OIDC group IDs and the group in Coder it should map to. This
|
A map of OIDC group IDs and the group in Coder it should map to. This
|
||||||
is useful for when OIDC providers only return group IDs.
|
is useful for when OIDC providers only return group IDs.
|
||||||
|
|
||||||
--oidc-ignore-email-verified bool, $CODER_OIDC_IGNORE_EMAIL_VERIFIED
|
--oidc-ignore-email-verified bool, $CODER_OIDC_IGNORE_EMAIL_VERIFIED
|
||||||
Ignore the email_verified claim from the upstream provider.
|
Ignore the email_verified claim from the upstream provider.
|
||||||
|
|
||||||
|
--oidc-ignore-userinfo bool, $CODER_OIDC_IGNORE_USERINFO (default: false)
|
||||||
|
Ignore the userinfo endpoint and only use the ID token for user
|
||||||
|
information.
|
||||||
|
|
||||||
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
|
--oidc-issuer-url string, $CODER_OIDC_ISSUER_URL
|
||||||
Issuer URL to use for Login with OIDC.
|
Issuer URL to use for Login with OIDC.
|
||||||
|
|
||||||
|
|||||||
Generated
+3
@@ -7553,6 +7553,9 @@ const docTemplate = `{
|
|||||||
"ignore_email_verified": {
|
"ignore_email_verified": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"ignore_user_info": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"issuer_url": {
|
"issuer_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+3
@@ -6767,6 +6767,9 @@
|
|||||||
"ignore_email_verified": {
|
"ignore_email_verified": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"ignore_user_info": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"issuer_url": {
|
"issuer_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+91
-33
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -483,6 +484,11 @@ type OIDCConfig struct {
|
|||||||
// AuthURLParams are additional parameters to be passed to the OIDC provider
|
// AuthURLParams are additional parameters to be passed to the OIDC provider
|
||||||
// when requesting an access token.
|
// when requesting an access token.
|
||||||
AuthURLParams map[string]string
|
AuthURLParams map[string]string
|
||||||
|
// IgnoreUserInfo causes Coder to only use claims from the ID token to
|
||||||
|
// process OIDC logins. This is useful if the OIDC provider does not
|
||||||
|
// support the userinfo endpoint, or if the userinfo endpoint causes
|
||||||
|
// undesirable behavior.
|
||||||
|
IgnoreUserInfo bool
|
||||||
// GroupField selects the claim field to be used as the created user's
|
// GroupField selects the claim field to be used as the created user's
|
||||||
// groups. If the group field is the empty string, then no group updates
|
// groups. If the group field is the empty string, then no group updates
|
||||||
// will ever come from the OIDC provider.
|
// will ever come from the OIDC provider.
|
||||||
@@ -551,46 +557,62 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.Logger.Debug(ctx, "got oidc claims",
|
||||||
|
slog.F("source", "id_token"),
|
||||||
|
slog.F("claim_fields", claimFields(claims)),
|
||||||
|
slog.F("blank", blankFields(claims)),
|
||||||
|
)
|
||||||
|
|
||||||
// Not all claims are necessarily embedded in the `id_token`.
|
// Not all claims are necessarily embedded in the `id_token`.
|
||||||
// In GitLab, the username is left empty and must be fetched in UserInfo.
|
// In GitLab, the username is left empty and must be fetched in UserInfo.
|
||||||
//
|
//
|
||||||
// The OIDC specification says claims can be in either place, so we fetch
|
// The OIDC specification says claims can be in either place, so we fetch
|
||||||
// user info and merge the two claim sets to be sure we have all of
|
// user info if required and merge the two claim sets to be sure we have
|
||||||
// the correct data.
|
// all of the correct data.
|
||||||
userInfo, err := api.OIDCConfig.Provider.UserInfo(ctx, oauth2.StaticTokenSource(state.Token))
|
//
|
||||||
if err == nil {
|
// Some providers (e.g. ADFS) do not support custom OIDC claims in the
|
||||||
userInfoClaims := map[string]interface{}{}
|
// UserInfo endpoint, so we allow users to disable it and only rely on the
|
||||||
err = userInfo.Claims(&userInfoClaims)
|
// ID token.
|
||||||
if err != nil {
|
if !api.OIDCConfig.IgnoreUserInfo {
|
||||||
|
userInfo, err := api.OIDCConfig.Provider.UserInfo(ctx, oauth2.StaticTokenSource(state.Token))
|
||||||
|
if err == nil {
|
||||||
|
userInfoClaims := map[string]interface{}{}
|
||||||
|
err = userInfo.Claims(&userInfoClaims)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Failed to unmarshal user info claims.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.Logger.Debug(ctx, "got oidc claims",
|
||||||
|
slog.F("source", "userinfo"),
|
||||||
|
slog.F("claim_fields", claimFields(userInfoClaims)),
|
||||||
|
slog.F("blank", blankFields(userInfoClaims)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Merge the claims from the ID token and the UserInfo endpoint.
|
||||||
|
// Information from UserInfo takes precedence.
|
||||||
|
claims = mergeClaims(claims, userInfoClaims)
|
||||||
|
|
||||||
|
// Log all of the field names after merging.
|
||||||
|
api.Logger.Debug(ctx, "got oidc claims",
|
||||||
|
slog.F("source", "merged"),
|
||||||
|
slog.F("claim_fields", claimFields(claims)),
|
||||||
|
slog.F("blank", blankFields(claims)),
|
||||||
|
)
|
||||||
|
} else if !strings.Contains(err.Error(), "user info endpoint is not supported by this provider") {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Failed to unmarshal user info claims.",
|
Message: "Failed to obtain user information claims.",
|
||||||
Detail: err.Error(),
|
Detail: "The attempt to fetch claims via the UserInfo endpoint failed: " + err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
// The OIDC provider does not support the UserInfo endpoint.
|
||||||
|
// This is not an error, but we should log it as it may mean
|
||||||
|
// that some claims are missing.
|
||||||
|
api.Logger.Warn(ctx, "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token")
|
||||||
}
|
}
|
||||||
for k, v := range userInfoClaims {
|
|
||||||
claims[k] = v
|
|
||||||
}
|
|
||||||
} else if !strings.Contains(err.Error(), "user info endpoint is not supported by this provider") {
|
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
||||||
Message: "Failed to obtain user information claims.",
|
|
||||||
Detail: "The OIDC provider returned no claims as part of the `id_token`. The attempt to fetch claims via the UserInfo endpoint failed: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log all of the field names returned in the ID token claims, and the
|
|
||||||
// userinfo returned from the provider.
|
|
||||||
{
|
|
||||||
fields := make([]string, 0, len(claims))
|
|
||||||
for f := range claims {
|
|
||||||
fields = append(fields, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Logger.Debug(ctx, "got oidc claims",
|
|
||||||
slog.F("user_info", userInfo),
|
|
||||||
slog.F("claim_fields", fields),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameRaw, ok := claims[api.OIDCConfig.UsernameField]
|
usernameRaw, ok := claims[api.OIDCConfig.UsernameField]
|
||||||
@@ -657,7 +679,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||||||
group, ok := groupInterface.(string)
|
group, ok := groupInterface.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: fmt.Sprintf("Invalid group type. Expected string, got: %t", emailRaw),
|
Message: fmt.Sprintf("Invalid group type. Expected string, got: %T", groupInterface),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -761,6 +783,42 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// claimFields returns the sorted list of fields in the claims map.
|
||||||
|
func claimFields(claims map[string]interface{}) []string {
|
||||||
|
fields := []string{}
|
||||||
|
for field := range claims {
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
sort.Strings(fields)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// blankFields returns the list of fields in the claims map that are
|
||||||
|
// an empty string.
|
||||||
|
func blankFields(claims map[string]interface{}) []string {
|
||||||
|
fields := make([]string, 0)
|
||||||
|
for field, value := range claims {
|
||||||
|
if valueStr, ok := value.(string); ok && valueStr == "" {
|
||||||
|
fields = append(fields, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(fields)
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeClaims merges the claims from a and b and returns the merged set.
|
||||||
|
// claims from b take precedence over claims from a.
|
||||||
|
func mergeClaims(a, b map[string]interface{}) map[string]interface{} {
|
||||||
|
c := make(map[string]interface{})
|
||||||
|
for k, v := range a {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range b {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
type oauthLoginParams struct {
|
type oauthLoginParams struct {
|
||||||
User database.User
|
User database.User
|
||||||
Link database.UserLink
|
Link database.UserLink
|
||||||
|
|||||||
@@ -501,6 +501,7 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
AvatarURL string
|
AvatarURL string
|
||||||
StatusCode int
|
StatusCode int
|
||||||
IgnoreEmailVerified bool
|
IgnoreEmailVerified bool
|
||||||
|
IgnoreUserInfo bool
|
||||||
}{{
|
}{{
|
||||||
Name: "EmailOnly",
|
Name: "EmailOnly",
|
||||||
IDTokenClaims: jwt.MapClaims{
|
IDTokenClaims: jwt.MapClaims{
|
||||||
@@ -643,6 +644,48 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AllowSignups: true,
|
AllowSignups: true,
|
||||||
StatusCode: http.StatusTemporaryRedirect,
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
|
}, {
|
||||||
|
Name: "UserInfoOverridesIDTokenClaims",
|
||||||
|
IDTokenClaims: jwt.MapClaims{
|
||||||
|
"email": "internaluser@internal.domain",
|
||||||
|
"email_verified": false,
|
||||||
|
},
|
||||||
|
UserInfoClaims: jwt.MapClaims{
|
||||||
|
"email": "externaluser@external.domain",
|
||||||
|
"email_verified": true,
|
||||||
|
"preferred_username": "user",
|
||||||
|
},
|
||||||
|
Username: "user",
|
||||||
|
AllowSignups: true,
|
||||||
|
IgnoreEmailVerified: false,
|
||||||
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
|
}, {
|
||||||
|
Name: "InvalidUserInfo",
|
||||||
|
IDTokenClaims: jwt.MapClaims{
|
||||||
|
"email": "internaluser@internal.domain",
|
||||||
|
"email_verified": false,
|
||||||
|
},
|
||||||
|
UserInfoClaims: jwt.MapClaims{
|
||||||
|
"email": 1,
|
||||||
|
},
|
||||||
|
AllowSignups: true,
|
||||||
|
IgnoreEmailVerified: false,
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}, {
|
||||||
|
Name: "IgnoreUserInfo",
|
||||||
|
IDTokenClaims: jwt.MapClaims{
|
||||||
|
"email": "user@internal.domain",
|
||||||
|
"email_verified": true,
|
||||||
|
"preferred_username": "user",
|
||||||
|
},
|
||||||
|
UserInfoClaims: jwt.MapClaims{
|
||||||
|
"email": "user.mcname@external.domain",
|
||||||
|
"preferred_username": "Mr. User McName",
|
||||||
|
},
|
||||||
|
Username: "user",
|
||||||
|
IgnoreUserInfo: true,
|
||||||
|
AllowSignups: true,
|
||||||
|
StatusCode: http.StatusTemporaryRedirect,
|
||||||
}} {
|
}} {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
@@ -654,6 +697,7 @@ func TestUserOIDC(t *testing.T) {
|
|||||||
config.AllowSignups = tc.AllowSignups
|
config.AllowSignups = tc.AllowSignups
|
||||||
config.EmailDomain = tc.EmailDomain
|
config.EmailDomain = tc.EmailDomain
|
||||||
config.IgnoreEmailVerified = tc.IgnoreEmailVerified
|
config.IgnoreEmailVerified = tc.IgnoreEmailVerified
|
||||||
|
config.IgnoreUserInfo = tc.IgnoreUserInfo
|
||||||
|
|
||||||
client := coderdtest.New(t, &coderdtest.Options{
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
Auditor: auditor,
|
Auditor: auditor,
|
||||||
|
|||||||
+12
-1
@@ -258,6 +258,7 @@ type OIDCConfig struct {
|
|||||||
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
UsernameField clibase.String `json:"username_field" typescript:",notnull"`
|
||||||
EmailField clibase.String `json:"email_field" typescript:",notnull"`
|
EmailField clibase.String `json:"email_field" typescript:",notnull"`
|
||||||
AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
|
AuthURLParams clibase.Struct[map[string]string] `json:"auth_url_params" typescript:",notnull"`
|
||||||
|
IgnoreUserInfo clibase.Bool `json:"ignore_user_info" typescript:",notnull"`
|
||||||
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
|
GroupField clibase.String `json:"groups_field" typescript:",notnull"`
|
||||||
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
|
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"`
|
||||||
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
|
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"`
|
||||||
@@ -882,6 +883,16 @@ when required by your organization's security policy.`,
|
|||||||
Group: &deploymentGroupOIDC,
|
Group: &deploymentGroupOIDC,
|
||||||
YAML: "authURLParams",
|
YAML: "authURLParams",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "OIDC Ignore UserInfo",
|
||||||
|
Description: "Ignore the userinfo endpoint and only use the ID token for user information.",
|
||||||
|
Flag: "oidc-ignore-userinfo",
|
||||||
|
Env: "CODER_OIDC_IGNORE_USERINFO",
|
||||||
|
Default: "false",
|
||||||
|
Value: &c.OIDC.IgnoreUserInfo,
|
||||||
|
Group: &deploymentGroupOIDC,
|
||||||
|
YAML: "ignoreUserInfo",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "OIDC Group Field",
|
Name: "OIDC Group Field",
|
||||||
Description: "Change the OIDC default 'groups' claim field. By default, will be 'groups' if present in the oidc scopes argument.",
|
Description: "Change the OIDC default 'groups' claim field. By default, will be 'groups' if present in the oidc scopes argument.",
|
||||||
@@ -901,7 +912,7 @@ when required by your organization's security policy.`,
|
|||||||
Name: "OIDC Group Mapping",
|
Name: "OIDC Group Mapping",
|
||||||
Description: "A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.",
|
Description: "A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.",
|
||||||
Flag: "oidc-group-mapping",
|
Flag: "oidc-group-mapping",
|
||||||
Env: "OIDC_GROUP_MAPPING",
|
Env: "CODER_OIDC_GROUP_MAPPING",
|
||||||
Default: "{}",
|
Default: "{}",
|
||||||
Value: &c.OIDC.GroupMapping,
|
Value: &c.OIDC.GroupMapping,
|
||||||
Group: &deploymentGroupOIDC,
|
Group: &deploymentGroupOIDC,
|
||||||
|
|||||||
+30
-6
@@ -144,6 +144,10 @@ while signing in via OIDC as a new user. Coder will log the claim fields
|
|||||||
returned by the upstream identity provider in a message containing the
|
returned by the upstream identity provider in a message containing the
|
||||||
string `got oidc claims`, as well as the user info returned.
|
string `got oidc claims`, as well as the user info returned.
|
||||||
|
|
||||||
|
> **Note:** If you need to ensure that Coder only uses information from
|
||||||
|
> the ID token and does not hit the UserInfo endpoint, you can set the
|
||||||
|
> configuration option `CODER_OIDC_IGNORE_USERINFO=true`.
|
||||||
|
|
||||||
### Email Addresses
|
### Email Addresses
|
||||||
|
|
||||||
By default, Coder will look for the OIDC claim named `email` and use that
|
By default, Coder will look for the OIDC claim named `email` and use that
|
||||||
@@ -262,17 +266,37 @@ Below are some details specific to individual OIDC providers.
|
|||||||
steps as described [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs)
|
steps as described [here.](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/development/msal/adfs-msal-web-app-web-api#app-registration-in-ad-fs)
|
||||||
- **Server Application**: Note the Client ID.
|
- **Server Application**: Note the Client ID.
|
||||||
- **Configure Application Credentials**: Note the Client Secret.
|
- **Configure Application Credentials**: Note the Client Secret.
|
||||||
- **Configure Web API**: Ensure the Client ID is set as the relying party identifier.
|
- **Configure Web API**: Set the Client ID as the relying party identifier.
|
||||||
- **Application Permissions**: Allow access to the claims `openid`, `email`, and `profile`.
|
- **Application Permissions**: Allow access to the claims `openid`, `email`, `profile`, and `allatclaims`.
|
||||||
1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note
|
1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note
|
||||||
the value for `issuer`.
|
the value for `issuer`.
|
||||||
> **Note:** This is usually of the form `https://adfs.corp/adfs/.well-known/openid-configuration`
|
> **Note:** This is usually of the form `https://adfs.corp/adfs/.well-known/openid-configuration`
|
||||||
1. In Coder's configuration file (or Helm values as appropriate), set the following
|
1. In Coder's configuration file (or Helm values as appropriate), set the following
|
||||||
environment variables or their corresponding CLI arguments:
|
environment variables or their corresponding CLI arguments:
|
||||||
|
|
||||||
- `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step.
|
- `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step.
|
||||||
- `CODER_OIDC_CLIENT_ID`: the Client ID from step 1.
|
- `CODER_OIDC_CLIENT_ID`: the Client ID from step 1.
|
||||||
- `CODER_OIDC_CLIENT_SECRET`: the Client Secret from step 1.
|
- `CODER_OIDC_CLIENT_SECRET`: the Client Secret from step 1.
|
||||||
- `CODER_OIDC_AUTH_URL_PARAMS`: set to `{"resource":"urn:microsoft:userinfo"}` ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)). OIDC logins will fail if this is not set.
|
- `CODER_OIDC_AUTH_URL_PARAMS`: set to
|
||||||
1. Ensure that Coder has the required OIDC claims by performing either of the below:
|
|
||||||
- Configure your federation server to reuturn both the `email` and `preferred_username` fields by [creating a custom claim rule](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims), or
|
```console
|
||||||
- Set `CODER_OIDC_EMAIL_FIELD="upn"`. This will use the User Principal Name as the user email, which is [guaranteed to be unique in an Active Directory Forest](https://learn.microsoft.com/en-us/windows/win32/ad/naming-properties#upn-format).
|
{"resource":"$CLIENT_ID"}
|
||||||
|
```
|
||||||
|
|
||||||
|
where `$CLIENT_ID` is the Client ID from step 1 ([see here](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios#:~:text=scope%E2%80%AFopenid.-,resource,-optional)).
|
||||||
|
This is required for the upstream OIDC provider to return the requested claims.
|
||||||
|
|
||||||
|
- `CODER_OIDC_IGNORE_USERINFO`: Set to `true`.
|
||||||
|
|
||||||
|
1. Configure [Issuance Transform Rules](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-rule-to-send-ldap-attributes-as-claims)
|
||||||
|
on your federation server to send the following claims:
|
||||||
|
|
||||||
|
- `preferred_username`: You can use e.g. "Display Name" as required.
|
||||||
|
- `email`: You can use e.g. the LDAP attribute "E-Mail-Addresses" as required.
|
||||||
|
- `email_verified`: Create a custom claim rule:
|
||||||
|
|
||||||
|
```console
|
||||||
|
=> issue(Type = "email_verified", Value = "true")
|
||||||
|
```
|
||||||
|
|
||||||
|
- (Optional) If using Group Sync, send the required groups in the configured groups claim field. See [here](https://stackoverflow.com/a/55570286) for an example.
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
|||||||
"user": {}
|
"user": {}
|
||||||
},
|
},
|
||||||
"ignore_email_verified": true,
|
"ignore_email_verified": true,
|
||||||
|
"ignore_user_info": true,
|
||||||
"issuer_url": "string",
|
"issuer_url": "string",
|
||||||
"scopes": ["string"],
|
"scopes": ["string"],
|
||||||
"sign_in_text": "string",
|
"sign_in_text": "string",
|
||||||
|
|||||||
@@ -1877,6 +1877,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||||||
"user": {}
|
"user": {}
|
||||||
},
|
},
|
||||||
"ignore_email_verified": true,
|
"ignore_email_verified": true,
|
||||||
|
"ignore_user_info": true,
|
||||||
"issuer_url": "string",
|
"issuer_url": "string",
|
||||||
"scopes": ["string"],
|
"scopes": ["string"],
|
||||||
"sign_in_text": "string",
|
"sign_in_text": "string",
|
||||||
@@ -2224,6 +2225,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||||||
"user": {}
|
"user": {}
|
||||||
},
|
},
|
||||||
"ignore_email_verified": true,
|
"ignore_email_verified": true,
|
||||||
|
"ignore_user_info": true,
|
||||||
"issuer_url": "string",
|
"issuer_url": "string",
|
||||||
"scopes": ["string"],
|
"scopes": ["string"],
|
||||||
"sign_in_text": "string",
|
"sign_in_text": "string",
|
||||||
@@ -2890,6 +2892,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||||||
"user": {}
|
"user": {}
|
||||||
},
|
},
|
||||||
"ignore_email_verified": true,
|
"ignore_email_verified": true,
|
||||||
|
"ignore_user_info": true,
|
||||||
"issuer_url": "string",
|
"issuer_url": "string",
|
||||||
"scopes": ["string"],
|
"scopes": ["string"],
|
||||||
"sign_in_text": "string",
|
"sign_in_text": "string",
|
||||||
@@ -2911,6 +2914,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
|
|||||||
| `groups_field` | string | false | | |
|
| `groups_field` | string | false | | |
|
||||||
| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
|
| `icon_url` | [clibase.URL](#clibaseurl) | false | | |
|
||||||
| `ignore_email_verified` | boolean | false | | |
|
| `ignore_email_verified` | boolean | false | | |
|
||||||
|
| `ignore_user_info` | boolean | false | | |
|
||||||
| `issuer_url` | string | false | | |
|
| `issuer_url` | string | false | | |
|
||||||
| `scopes` | array of string | false | | |
|
| `scopes` | array of string | false | | |
|
||||||
| `sign_in_text` | string | false | | |
|
| `sign_in_text` | string | false | | |
|
||||||
|
|||||||
+11
-1
@@ -361,7 +361,7 @@ Change the OIDC default 'groups' claim field. By default, will be 'groups' if pr
|
|||||||
| | |
|
| | |
|
||||||
| ----------- | -------------------------------------- |
|
| ----------- | -------------------------------------- |
|
||||||
| Type | <code>struct[map[string]string]</code> |
|
| Type | <code>struct[map[string]string]</code> |
|
||||||
| Environment | <code>$OIDC_GROUP_MAPPING</code> |
|
| Environment | <code>$CODER_OIDC_GROUP_MAPPING</code> |
|
||||||
| Default | <code>{}</code> |
|
| Default | <code>{}</code> |
|
||||||
|
|
||||||
A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.
|
A map of OIDC group IDs and the group in Coder it should map to. This is useful for when OIDC providers only return group IDs.
|
||||||
@@ -384,6 +384,16 @@ URL pointing to the icon to use on the OepnID Connect login button.
|
|||||||
|
|
||||||
Ignore the email_verified claim from the upstream provider.
|
Ignore the email_verified claim from the upstream provider.
|
||||||
|
|
||||||
|
### --oidc-ignore-userinfo
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | ---------------------------------------- |
|
||||||
|
| Type | <code>bool</code> |
|
||||||
|
| Environment | <code>$CODER_OIDC_IGNORE_USERINFO</code> |
|
||||||
|
| Default | <code>false</code> |
|
||||||
|
|
||||||
|
Ignore the userinfo endpoint and only use the ID token for user information.
|
||||||
|
|
||||||
### --oidc-issuer-url
|
### --oidc-issuer-url
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ export interface OIDCConfig {
|
|||||||
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
|
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||||
readonly auth_url_params: any
|
readonly auth_url_params: any
|
||||||
|
readonly ignore_user_info: boolean
|
||||||
readonly groups_field: string
|
readonly groups_field: string
|
||||||
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
|
// Named type "github.com/coder/coder/cli/clibase.Struct[map[string]string]" unknown, using "any"
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- External type
|
||||||
|
|||||||
Reference in New Issue
Block a user