- Notifications
You must be signed in to change notification settings - Fork1k
fix(coderd)!: add CODER_OIDC_IGNORE_USERINFO configuration option#6922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
1b0b274
20991bb
38f1714
66e8df9
780f516
8e8a7a2
dfde7c7
efee87d
50df5d0
3c3effd
d0b832f
6901563
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -7,6 +7,7 @@ import ( | ||
"fmt" | ||
"net/http" | ||
"net/mail" | ||
"sort" | ||
"strconv" | ||
"strings" | ||
@@ -483,6 +484,11 @@ type OIDCConfig struct { | ||
// AuthURLParams are additional parameters to be passed to the OIDC provider | ||
// when requesting an access token. | ||
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 | ||
// groups. If the group field is the empty string, then no group updates | ||
// will ever come from the OIDC provider. | ||
@@ -551,46 +557,62 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { | ||
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`. | ||
// 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 | ||
// user info if required and merge the two claim sets to be sure we have | ||
// all of the correct data. | ||
// | ||
// Some providers (e.g. ADFS) do not support custom OIDC claims in the | ||
// UserInfo endpoint, so we allow users to disable it and only rely on the | ||
// ID token. | ||
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{ | ||
Message: "Failed toobtain userinformation claims.", | ||
Detail:"The attempt to fetch claims via the UserInfo endpoint failed: " +err.Error(), | ||
}) | ||
return | ||
} else { | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
// 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") | ||
Comment on lines +611 to +614 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Should we be printing an error here too? We don't have the required claims at this point, so it seems odd to just warn with logs but proceed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. If we're going to error, I'd say we should just fail the request outright. I could remove the check for the username field as a "required" claim, WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Ahh, I see. Up to you, I'm really not sure. Kinda uncharted territory for me. MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Instead of making the logic "magical", maybe a better choice is adding an option | ||
} | ||
MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. NOTE: we were previously logging all of the userinfo claims here; this could contain sensitive user information. I modified this to instead log the claim fields, and which of these claim fields are an empty string. | ||
} | ||
usernameRaw, ok := claims[api.OIDCConfig.UsernameField] | ||
@@ -657,7 +679,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { | ||
group, ok := groupInterface.(string) | ||
if !ok { | ||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ | ||
Message: fmt.Sprintf("Invalid group type. Expected string, got: %T",groupInterface), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. NOTE: I'm assuming this was what the original author intended here | ||
}) | ||
return | ||
} | ||
@@ -761,6 +783,42 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { | ||
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) | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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 { | ||
User database.User | ||
Link database.UserLink | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -257,6 +257,7 @@ type OIDCConfig struct { | ||
UsernameField clibase.String `json:"username_field" typescript:",notnull"` | ||
EmailField clibase.String `json:"email_field" 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"` | ||
GroupMapping clibase.Struct[map[string]string] `json:"group_mapping" typescript:",notnull"` | ||
SignInText clibase.String `json:"sign_in_text" typescript:",notnull"` | ||
@@ -881,6 +882,16 @@ when required by your organization's security policy.`, | ||
Group: &deploymentGroupOIDC, | ||
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", | ||
Description: "Change the OIDC default 'groups' claim field. By default, will be 'groups' if present in the oidc scopes argument.", | ||
@@ -900,7 +911,7 @@ when required by your organization's security policy.`, | ||
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.", | ||
Flag: "oidc-group-mapping", | ||
Env: "CODER_OIDC_GROUP_MAPPING", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Oops | ||
Default: "{}", | ||
Value: &c.OIDC.GroupMapping, | ||
Group: &deploymentGroupOIDC, | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
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 | ||
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) | ||
- **Server Application**: Note the Client ID. | ||
- **Configure Application Credentials**: Note the Client Secret. | ||
- **Configure Web API**:Set the Client ID as the relying party identifier. | ||
- **Application Permissions**: Allow access to the claims `openid`, `email`,`profile`,and `allatclaims`. | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
1. Visit your ADFS server's `/.well-known/openid-configuration` URL and note | ||
the value for `issuer`. | ||
> **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 | ||
environment variables or their corresponding CLI arguments: | ||
- `CODER_OIDC_ISSUER_URL`: the `issuer` value from the previous step. | ||
- `CODER_OIDC_CLIENT_ID`: the Client ID from step 1. | ||
- `CODER_OIDC_CLIENT_SECRET`: the Client Secret from step 1. | ||
- `CODER_OIDC_AUTH_URL_PARAMS`: set to | ||
```console | ||
{"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) | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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. |
Uh oh!
There was an error while loading.Please reload this page.