feat: add support for additional OIDC claims via oidcConfig.additionalClaims
This commit is contained in:
parent
a279fece02
commit
ade3bc54f8
|
|
@ -9,6 +9,7 @@
|
|||
## Changes since v7.14.2
|
||||
|
||||
- [#3183](https://github.com/oauth2-proxy/oauth2-proxy/pull/3183) fix: allow URL parameters to configure username, password and max idle connection timeout if the matching configuration is empty.
|
||||
- [#3340](https://github.com/oauth2-proxy/oauth2-proxy/issues/3340) feat: allow injecting additional OIDC claims via `oidcConfig.additionalClaims`
|
||||
|
||||
# V7.14.2
|
||||
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
|
||||
## Important Notes
|
||||
|
||||
This release introduces a breaking change for Alpha Config users and moves us significantly
|
||||
This release introduces a breaking change for Alpha Config users and moves us significantly
|
||||
closer to removing legacy configuration parameters, making the codebase of OAuth2 Proxy more
|
||||
future proof and extensible.
|
||||
|
||||
|
|
@ -137,7 +138,7 @@ the project for future maintainability and future improvements like structured l
|
|||
- [#3292](https://github.com/oauth2-proxy/oauth2-proxy/pull/3292) chore(deps): upgrade gomod and bump to golang v1.25.5 (@tuunit)
|
||||
- [#3304](https://github.com/oauth2-proxy/oauth2-proxy/pull/3304) fix: added conditional so default is not always set and env vars are honored fixes 3303 (@pixeldrew)
|
||||
- [#3264](https://github.com/oauth2-proxy/oauth2-proxy/pull/3264) fix: more aggressively truncate logged access_token (@MartinNowak / @tuunit)
|
||||
- [#3267](https://github.com/oauth2-proxy/oauth2-proxy/pull/3267) fix: Session refresh handling in OIDC provider (@gysel)
|
||||
- [#3267](https://github.com/oauth2-proxy/oauth2-proxy/pull/3267) fix: Session refresh handling in OIDC provider (@gysel)
|
||||
- [#3290](https://github.com/oauth2-proxy/oauth2-proxy/pull/3290) fix: WebSocket proxy to respect PassHostHeader setting (@UnsignedLong)
|
||||
|
||||
# V7.13.0
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ injectResponseHeaders:
|
|||
value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk # base64 encoded password
|
||||
```
|
||||
|
||||
**Value sources:**
|
||||
**Value sources:**
|
||||
* `claimSource` - `claim` (session claims either from id token or from profile URL)
|
||||
* `secretSource` - `value` (base64), `fromFile` (file path)
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ ClaimSource allows loading a header value from a claim within the session
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. Available claims: `access_token` `id_token` `created_at`<br/>`expires_on` `refresh_token` `email` `user` `groups` `preferred_username`. |
|
||||
| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. Available claims: `access_token` `id_token` `created_at`<br/>`expires_on` `refresh_token` `email` `user` `groups` `preferred_username`,<br/>plus any custom claims configured in `oidcConfig.additionalClaims`. |
|
||||
| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
|
||||
| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
|
||||
|
||||
|
|
@ -488,6 +488,7 @@ character.
|
|||
| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
|
||||
| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
|
||||
| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
|
||||
| `additionalClaims` | _[]string_ | AdditionalClaims is a list of additional claim names to pull from the ID token<br/>or profile URL and store in the session for use with ClaimSource. |
|
||||
|
||||
### Provider
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ injectResponseHeaders:
|
|||
value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk # base64 encoded password
|
||||
```
|
||||
|
||||
**Value sources:**
|
||||
**Value sources:**
|
||||
* `claimSource` - `claim` (session claims either from id token or from profile URL)
|
||||
* `secretSource` - `value` (base64), `fromFile` (file path)
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ ClaimSource allows loading a header value from a claim within the session
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. Available claims: `access_token` `id_token` `created_at`<br/>`expires_on` `refresh_token` `email` `user` `groups` `preferred_username`. |
|
||||
| `claim` | _string_ | Claim is the name of the claim in the session that the value should be<br/>loaded from. Available claims: `access_token` `id_token` `created_at`<br/>`expires_on` `refresh_token` `email` `user` `groups` `preferred_username`,<br/>plus any custom claims configured in `oidcConfig.additionalClaims`. |
|
||||
| `prefix` | _string_ | Prefix is an optional prefix that will be prepended to the value of the<br/>claim if it is non-empty. |
|
||||
| `basicAuthPassword` | _[SecretSource](#secretsource)_ | BasicAuthPassword converts this claim into a basic auth header.<br/>Note the value of claim will become the basic auth username and the<br/>basicAuthPassword will be used as the password value. |
|
||||
|
||||
|
|
@ -488,6 +488,7 @@ character.
|
|||
| `userIDClaim` | _string_ | UserIDClaim indicates which claim contains the user ID<br/>default set to 'email' |
|
||||
| `audienceClaims` | _[]string_ | AudienceClaim allows to define any claim that is verified against the client id<br/>By default `aud` claim is used for verification. |
|
||||
| `extraAudiences` | _[]string_ | ExtraAudiences is a list of additional audiences that are allowed<br/>to pass verification in addition to the client id. |
|
||||
| `additionalClaims` | _[]string_ | AdditionalClaims is a list of additional claim names to pull from the ID token<br/>or profile URL and store in the session for use with ClaimSource. |
|
||||
|
||||
### Provider
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ type HeaderValue struct {
|
|||
type ClaimSource struct {
|
||||
// Claim is the name of the claim in the session that the value should be
|
||||
// loaded from. Available claims: `access_token` `id_token` `created_at`
|
||||
// `expires_on` `refresh_token` `email` `user` `groups` `preferred_username`.
|
||||
// `expires_on` `refresh_token` `email` `user` `groups` `preferred_username`,
|
||||
// plus any custom claims configured in `oidcConfig.additionalClaims`.
|
||||
Claim string `yaml:"claim,omitempty"`
|
||||
|
||||
// Prefix is an optional prefix that will be prepended to the value of the
|
||||
|
|
|
|||
|
|
@ -318,6 +318,9 @@ type OIDCOptions struct {
|
|||
// ExtraAudiences is a list of additional audiences that are allowed
|
||||
// to pass verification in addition to the client id.
|
||||
ExtraAudiences []string `yaml:"extraAudiences,omitempty"`
|
||||
// AdditionalClaims defines additional claims to pull from the ID token or
|
||||
// profile URL and store in the session for claimSource usage.
|
||||
AdditionalClaims []string `yaml:"additionalClaims,omitempty"`
|
||||
}
|
||||
|
||||
type LoginGovOptions struct {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type SessionState struct {
|
|||
User string `msgpack:"u,omitempty"`
|
||||
Groups []string `msgpack:"g,omitempty"`
|
||||
PreferredUsername string `msgpack:"pu,omitempty"`
|
||||
ExtraClaims map[string][]string `msgpack:"ec,omitempty"`
|
||||
|
||||
// Internal helpers, not serialized
|
||||
Clock func() time.Time `msgpack:"-"` // override for time.Now, for testing
|
||||
|
|
@ -156,7 +157,14 @@ func (s *SessionState) GetClaim(claim string) []string {
|
|||
case "preferred_username":
|
||||
return []string{s.PreferredUsername}
|
||||
default:
|
||||
return []string{}
|
||||
if s.ExtraClaims == nil {
|
||||
return []string{}
|
||||
}
|
||||
values, ok := s.ExtraClaims[claim]
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
return append([]string(nil), values...)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,26 @@ func TestAge(t *testing.T) {
|
|||
assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
|
||||
}
|
||||
|
||||
func TestGetClaimAdditionalClaims(t *testing.T) {
|
||||
ss := &SessionState{
|
||||
ExtraClaims: map[string][]string{
|
||||
"displayName": []string{"Jane D."},
|
||||
"jobTitle": []string{"Principal Consultant"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"Jane D."}, ss.GetClaim("displayName"))
|
||||
assert.Equal(t, []string{"Principal Consultant"}, ss.GetClaim("jobTitle"))
|
||||
assert.Equal(t, []string{}, ss.GetClaim("missing"))
|
||||
|
||||
result := ss.GetClaim("displayName")
|
||||
result[0] = "Mutated"
|
||||
assert.Equal(t, []string{"Jane D."}, ss.GetClaim("displayName"))
|
||||
|
||||
var nilSession *SessionState
|
||||
assert.Equal(t, []string{}, nilSession.GetClaim("displayName"))
|
||||
}
|
||||
|
||||
// TestEncodeAndDecodeSessionState encodes & decodes various session states
|
||||
// and confirms the operation is 1:1
|
||||
func TestEncodeAndDecodeSessionState(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -180,6 +180,37 @@ var _ = Describe("Headers Suite", func() {
|
|||
},
|
||||
expectedErr: "",
|
||||
}),
|
||||
Entry("with configured additional claims in response headers", headersTableInput{
|
||||
headers: []options.Header{
|
||||
{
|
||||
Name: "X-User-Display-Name",
|
||||
Values: []options.HeaderValue{{
|
||||
ClaimSource: &options.ClaimSource{Claim: "displayName"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Name: "X-User-Job-Title",
|
||||
Values: []options.HeaderValue{{
|
||||
ClaimSource: &options.ClaimSource{Claim: "jobTitle"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
initialHeaders: http.Header{
|
||||
"Foo": []string{"bar", "baz"},
|
||||
},
|
||||
session: &sessionsapi.SessionState{
|
||||
ExtraClaims: map[string][]string{
|
||||
"displayName": []string{"Jane D."},
|
||||
"jobTitle": []string{"Principal Consultant"},
|
||||
},
|
||||
},
|
||||
expectedHeaders: http.Header{
|
||||
"Foo": []string{"bar,baz"},
|
||||
"X-User-Display-Name": []string{"Jane D."},
|
||||
"X-User-Job-Title": []string{"Principal Consultant"},
|
||||
},
|
||||
expectedErr: "",
|
||||
}),
|
||||
Entry("with an invalid basicAuthPassword claim valued header", headersTableInput{
|
||||
headers: []options.Header{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ type ProviderData struct {
|
|||
GroupsClaim string
|
||||
Verifier internaloidc.IDTokenVerifier
|
||||
SkipClaimsFromProfileURL bool
|
||||
AdditionalClaims []string
|
||||
|
||||
// Universal Group authorization data structure
|
||||
// any provider can set to consume
|
||||
|
|
@ -268,6 +269,23 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*
|
|||
}
|
||||
}
|
||||
|
||||
if len(p.AdditionalClaims) > 0 {
|
||||
for _, claim := range p.AdditionalClaims {
|
||||
var values []string
|
||||
exists, err := extractor.GetClaimInto(claim, &values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
if ss.ExtraClaims == nil {
|
||||
ss.ExtraClaims = map[string][]string{}
|
||||
}
|
||||
ss.ExtraClaims[claim] = append([]string(nil), values...)
|
||||
}
|
||||
}
|
||||
|
||||
// `email_verified` must be present and explicitly set to `false` to be
|
||||
// considered unverified.
|
||||
verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
|
||||
|
|
|
|||
|
|
@ -95,6 +95,20 @@ var (
|
|||
RegisteredClaims: registeredClaims,
|
||||
}
|
||||
|
||||
displayNameAndJobTitleIDToken = idTokenClaims{
|
||||
Name: "Jane Dobbs",
|
||||
Email: "janed@me.com",
|
||||
Phone: "+4798765432",
|
||||
Picture: "http://mugbook.com/janed/me.jpg",
|
||||
Groups: []string{"test:a", "test:b"},
|
||||
Roles: []string{"test:c", "test:d"},
|
||||
DisplayName: "Jane D.",
|
||||
JobTitle: "Principal Consultant",
|
||||
Verified: &verified,
|
||||
Nonce: encryption.HashNonce([]byte(oidcNonce)),
|
||||
RegisteredClaims: registeredClaims,
|
||||
}
|
||||
|
||||
unverifiedIDToken = idTokenClaims{
|
||||
Name: "Mystery Man",
|
||||
Email: "unverified@email.com",
|
||||
|
|
@ -118,6 +132,8 @@ type idTokenClaims struct {
|
|||
Picture string `json:"picture,omitempty"`
|
||||
Groups interface{} `json:"groups,omitempty"`
|
||||
Roles interface{} `json:"roles,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
JobTitle string `json:"jobTitle,omitempty"`
|
||||
Verified *bool `json:"email_verified,omitempty"`
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
|
|
@ -234,6 +250,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
GroupsClaim string
|
||||
SkipClaimsFromProfileURL bool
|
||||
SetProfileURL bool
|
||||
AdditionalClaims []string
|
||||
ExpectedError error
|
||||
ExpectedSession *sessions.SessionState
|
||||
ExpectProfileURLCalled bool
|
||||
|
|
@ -405,6 +422,42 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
PreferredUsername: "Jane Dobbs",
|
||||
},
|
||||
},
|
||||
"Extra Claims": {
|
||||
IDToken: defaultIDToken,
|
||||
AllowUnverified: true,
|
||||
EmailClaim: "email",
|
||||
GroupsClaim: "groups",
|
||||
UserClaim: "sub",
|
||||
AdditionalClaims: []string{"picture", "roles"},
|
||||
ExpectedSession: &sessions.SessionState{
|
||||
User: "123456789",
|
||||
Email: "janed@me.com",
|
||||
Groups: []string{"test:a", "test:b"},
|
||||
PreferredUsername: "Jane Dobbs",
|
||||
ExtraClaims: map[string][]string{
|
||||
"picture": {"http://mugbook.com/janed/me.jpg"},
|
||||
"roles": {"test:c", "test:d"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Extra Claims (displayName and jobTitle)": {
|
||||
IDToken: displayNameAndJobTitleIDToken,
|
||||
AllowUnverified: true,
|
||||
EmailClaim: "email",
|
||||
GroupsClaim: "groups",
|
||||
UserClaim: "sub",
|
||||
AdditionalClaims: []string{"displayName", "jobTitle"},
|
||||
ExpectedSession: &sessions.SessionState{
|
||||
User: "123456789",
|
||||
Email: "janed@me.com",
|
||||
Groups: []string{"test:a", "test:b"},
|
||||
PreferredUsername: "Jane Dobbs",
|
||||
ExtraClaims: map[string][]string{
|
||||
"displayName": {"Jane D."},
|
||||
"jobTitle": {"Principal Consultant"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"Request claims from ProfileURL": {
|
||||
IDToken: minimalIDToken,
|
||||
SetProfileURL: true,
|
||||
|
|
@ -453,6 +506,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
provider.EmailClaim = tc.EmailClaim
|
||||
provider.GroupsClaim = tc.GroupsClaim
|
||||
provider.SkipClaimsFromProfileURL = tc.SkipClaimsFromProfileURL
|
||||
provider.AdditionalClaims = tc.AdditionalClaims
|
||||
|
||||
rawIDToken, err := newSignedTestIDToken(tc.IDToken)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
|
|||
p.EmailClaim = providerConfig.OIDCConfig.EmailClaim
|
||||
p.GroupsClaim = providerConfig.OIDCConfig.GroupsClaim
|
||||
p.SkipClaimsFromProfileURL = ptr.Deref(providerConfig.SkipClaimsFromProfileURL, options.DefaultSkipClaimsFromProfileURL)
|
||||
p.AdditionalClaims = append([]string(nil), providerConfig.OIDCConfig.AdditionalClaims...)
|
||||
|
||||
// Set PKCE enabled or disabled based on discovery and force options
|
||||
p.CodeChallengeMethod = parseCodeChallengeMethod(providerConfig)
|
||||
|
|
|
|||
Loading…
Reference in New Issue