Skip to content

Commit

Permalink
improved audience handling to support client credentials access token…
Browse files Browse the repository at this point in the history
…s without aud claims (oauth2-proxy#1204)

* implementation draft

* add cfg options skip-au-when-missing && client-id-verification-claim; enhance the provider data verification logic for sake of the added options

* refactor configs, added logging and add additional claim verification

* simplify logic by just having one configuration similar to oidc-email-claim

* added internal oidc token verifier, so that aud check behavior can be managed with oauth2-proxy and is compatible with extra-jwt-issuers

* refactored verification to reduce complexity

* refactored verification to reduce complexity

* added docs

* adjust tests to support new OIDCAudienceClaim and OIDCExtraAudiences options

* extend unit tests and ensure that audience is set with the value of aud claim configuration

* revert filemodes and update docs

* update docs

* remove unneccesary logging, refactor audience existence check and added additional unit tests

* fix linting issues after rebase on origin/main

* cleanup: use new imports for migrated libraries after rebase on origin/main

* adapt mock in keycloak_oidc_test.go

* allow specifying multiple audience claims, fixed bug where jwt issuers client id was not the being considered and fixed bug where aud claims with multiple audiences has broken the whole validation

* fixed formatting issue

* do not pass the whole options struct to minimize complexity and dependency to the configuration structure

* added changelog entry

* update docs

Co-authored-by: Sofia Weiler <sofia.weiler@aoe.com>
Co-authored-by: Christian Zenker <christian.zenker@aoe.com>
  • Loading branch information
3 people authored Feb 15, 2022
1 parent c5a98c6 commit 25371ea
Show file tree
Hide file tree
Showing 20 changed files with 536 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- [#1489](https://github.com/oauth2-proxy/oauth2-proxy/pull/1489) Fix Docker Buildx push to include build version (@JoelSpeed)
- [#1477](https://github.com/oauth2-proxy/oauth2-proxy/pull/1477) Remove provider documentation for `Microsoft Azure AD` (@omBratteng)
- [#1204](https://github.com/oauth2-proxy/oauth2-proxy/pull/1204) Added configuration for audience claim (`--oidc-extra-audience`) and ability to specify extra audiences (`--oidc-extra-audience`) allowed passing audience verification. This enables support for AWS Cognito and other issuers that have custom audience claims. Also, this adds the ability to allow multiple audiences. (@kschu91)
- [#1509](https://github.com/oauth2-proxy/oauth2-proxy/pull/1509) Update LoginGovProvider ValidateSession to pass access_token in Header (@pksheldon4)
- [#1474](https://github.com/oauth2-proxy/oauth2-proxy/pull/1474) Support configuration of minimal acceptable TLS version (@polarctos)
- [#1545](https://github.com/oauth2-proxy/oauth2-proxy/pull/1545) Fix issue with query string allowed group panic on skip methods (@andytson)
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/configuration/alpha_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ make up the header value
| `emailClaim` | _string_ | EmailClaim indicates which claim contains the user email,<br/>default set to 'email' |
| `groupsClaim` | _string_ | GroupsClaim indicates which claim contains the user groups<br/>default set to 'groups' |
| `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. |

### Provider

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/configuration/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
| `--oidc-jwks-url` | string | OIDC JWKS URI for token verification; required if OIDC discovery is disabled | |
| `--oidc-email-claim` | string | which OIDC claim contains the user's email | `"email"` |
| `--oidc-groups-claim` | string | which OIDC claim contains the user groups | `"groups"` |
| `--oidc-audience-claim` | string | which OIDC claim contains the audience | `"aud"` |
| `--oidc-extra-audience` | string \| list | additional audiences which are allowed to pass verification | `"[]"` |
| `--pass-access-token` | bool | pass OAuth access_token to upstream via X-Forwarded-Access-Token header. When used with `--set-xauthrequest` this adds the X-Auth-Request-Access-Token header to the response | false |
| `--pass-authorization-header` | bool | pass OIDC IDToken to upstream via Authorization Bearer header | false |
| `--pass-basic-auth` | bool | pass HTTP Basic Auth, X-Forwarded-User, X-Forwarded-Email and X-Forwarded-Preferred-Username information to upstream | true |
Expand Down
4 changes: 4 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ providers:
emailClaim: email
userIDClaim: email
insecureSkipNonce: true
audienceClaims: [aud]
extraAudiences: []
`

const testCoreConfig = `
Expand Down Expand Up @@ -148,6 +150,8 @@ redirect_url="http://localhost:4180/oauth2/callback"
GroupsClaim: "groups",
EmailClaim: "email",
UserIDClaim: "email",
AudienceClaims: []string{"aud"},
ExtraAudiences: []string{},
InsecureSkipNonce: true,
},
ApprovalPrompt: "force",
Expand Down
12 changes: 10 additions & 2 deletions oauthproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation"
Expand Down Expand Up @@ -1737,7 +1738,14 @@ func TestGetJwtSession(t *testing.T) {

keyset := NoOpKeySet{}
verifier := oidc.NewVerifier("https://issuer.example.com", keyset,
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true})
&oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true,
SkipClientIDCheck: true})
verificationOptions := &internaloidc.IDTokenVerificationOptions{
AudienceClaims: []string{"aud"},
ClientID: "https://test.myapp.com",
ExtraAudiences: []string{},
}
internalVerifier := internaloidc.NewVerifier(verifier, verificationOptions)

test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) {
opts.InjectRequestHeaders = []options.Header{
Expand Down Expand Up @@ -1808,7 +1816,7 @@ func TestGetJwtSession(t *testing.T) {
},
}
opts.SkipJwtBearerTokens = true
opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), verifier))
opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), internalVerifier))
})
if err != nil {
t.Fatal(err)
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/options/legacy_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func NewLegacyOptions() *LegacyOptions {
UserIDClaim: "email",
OIDCEmailClaim: "email",
OIDCGroupsClaim: "groups",
OIDCAudienceClaims: []string{"aud"},
OIDCExtraAudiences: []string{},
InsecureOIDCSkipNonce: true,
},

Expand Down Expand Up @@ -500,6 +502,8 @@ type LegacyProvider struct {
OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"`
OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"`
OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"`
OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"`
OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"`
LoginURL string `flag:"login-url" cfg:"login_url"`
RedeemURL string `flag:"redeem-url" cfg:"redeem_url"`
ProfileURL string `flag:"profile-url" cfg:"profile_url"`
Expand Down Expand Up @@ -550,6 +554,8 @@ func legacyProviderFlagSet() *pflag.FlagSet {
flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)")
flagSet.String("oidc-groups-claim", providers.OIDCGroupsClaim, "which OIDC claim contains the user groups")
flagSet.String("oidc-email-claim", providers.OIDCEmailClaim, "which OIDC claim contains the user's email")
flagSet.StringSlice("oidc-audience-claim", providers.OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id")
flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification")
flagSet.String("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint")
Expand Down Expand Up @@ -644,6 +650,8 @@ func (l *LegacyProvider) convert() (Providers, error) {
UserIDClaim: l.UserIDClaim,
EmailClaim: l.OIDCEmailClaim,
GroupsClaim: l.OIDCGroupsClaim,
AudienceClaims: l.OIDCAudienceClaims,
ExtraAudiences: l.OIDCExtraAudiences,
}

// This part is out of the switch section because azure has a default tenant
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/options/legacy_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ var _ = Describe("Legacy Options", func() {
opts.Providers[0].ClientID = "oauth-proxy"
opts.Providers[0].ID = "google=oauth-proxy"
opts.Providers[0].OIDCConfig.InsecureSkipNonce = true
opts.Providers[0].OIDCConfig.AudienceClaims = []string{"aud"}
opts.Providers[0].OIDCConfig.ExtraAudiences = []string{}

converted, err := legacyOpts.ToOptions()
Expect(err).ToNot(HaveOccurred())
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/options/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var _ = Describe("Load", func() {
UserIDClaim: "email",
OIDCEmailClaim: "email",
OIDCGroupsClaim: "groups",
OIDCAudienceClaims: []string{"aud"},
InsecureOIDCSkipNonce: true,
},

Expand Down
30 changes: 17 additions & 13 deletions pkg/apis/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"crypto"
"net/url"

"github.com/coreos/go-oidc/v3/oidc"
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -70,25 +70,29 @@ type Options struct {
redirectURL *url.URL
provider providers.Provider
signatureData *SignatureData
oidcVerifier *oidc.IDTokenVerifier
jwtBearerVerifiers []*oidc.IDTokenVerifier
oidcVerifier *internaloidc.IDTokenVerifier
jwtBearerVerifiers []*internaloidc.IDTokenVerifier
realClientIPParser ipapi.RealClientIPParser
}

// Options for Getting internal values
func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL }
func (o *Options) GetProvider() providers.Provider { return o.provider }
func (o *Options) GetSignatureData() *SignatureData { return o.signatureData }
func (o *Options) GetOIDCVerifier() *oidc.IDTokenVerifier { return o.oidcVerifier }
func (o *Options) GetJWTBearerVerifiers() []*oidc.IDTokenVerifier { return o.jwtBearerVerifiers }
func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL }
func (o *Options) GetProvider() providers.Provider { return o.provider }
func (o *Options) GetSignatureData() *SignatureData { return o.signatureData }
func (o *Options) GetOIDCVerifier() *internaloidc.IDTokenVerifier { return o.oidcVerifier }
func (o *Options) GetJWTBearerVerifiers() []*internaloidc.IDTokenVerifier {
return o.jwtBearerVerifiers
}
func (o *Options) GetRealClientIPParser() ipapi.RealClientIPParser { return o.realClientIPParser }

// Options for Setting internal values
func (o *Options) SetRedirectURL(s *url.URL) { o.redirectURL = s }
func (o *Options) SetProvider(s providers.Provider) { o.provider = s }
func (o *Options) SetSignatureData(s *SignatureData) { o.signatureData = s }
func (o *Options) SetOIDCVerifier(s *oidc.IDTokenVerifier) { o.oidcVerifier = s }
func (o *Options) SetJWTBearerVerifiers(s []*oidc.IDTokenVerifier) { o.jwtBearerVerifiers = s }
func (o *Options) SetRedirectURL(s *url.URL) { o.redirectURL = s }
func (o *Options) SetProvider(s providers.Provider) { o.provider = s }
func (o *Options) SetSignatureData(s *SignatureData) { o.signatureData = s }
func (o *Options) SetOIDCVerifier(s *internaloidc.IDTokenVerifier) { o.oidcVerifier = s }
func (o *Options) SetJWTBearerVerifiers(s []*internaloidc.IDTokenVerifier) {
o.jwtBearerVerifiers = s
}
func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClientIPParser = s }

// NewOptions constructs a new Options with defaulted values
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/options/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ type OIDCOptions struct {
// UserIDClaim indicates which claim contains the user ID
// default set to 'email'
UserIDClaim string `json:"userIDClaim,omitempty"`
// AudienceClaim allows to define any claim that is verified against the client id
// By default `aud` claim is used for verification.
AudienceClaims []string `json:"audienceClaims,omitempty"`
// ExtraAudiences is a list of additional audiences that are allowed
// to pass verification in addition to the client id.
ExtraAudiences []string `json:"extraAudiences,omitempty"`
}

type LoginGovOptions struct {
Expand Down Expand Up @@ -191,6 +197,8 @@ func providerDefaults() Providers {
UserIDClaim: providers.OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim
EmailClaim: providers.OIDCEmailClaim,
GroupsClaim: providers.OIDCGroupsClaim,
AudienceClaims: providers.OIDCAudienceClaims,
ExtraAudiences: []string{},
},
},
}
Expand Down
16 changes: 16 additions & 0 deletions pkg/oidc/oidc_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package oidc

import (
"testing"

"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestOIDCSuite(t *testing.T) {
logger.SetOutput(GinkgoWriter)

RegisterFailHandler(Fail)
RunSpecs(t, "OIDC")
}
100 changes: 100 additions & 0 deletions pkg/oidc/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package oidc

import (
"context"
"fmt"
"reflect"

"github.com/coreos/go-oidc/v3/oidc"
)

// IDTokenVerifier Used to verify an ID Token and extends oidc.IDTokenVerifier from the underlying oidc library
type IDTokenVerifier struct {
*oidc.IDTokenVerifier
*IDTokenVerificationOptions
allowedAudiences map[string]struct{}
}

// IDTokenVerificationOptions options for the oidc.IDTokenVerifier that are required to verify an ID Token
type IDTokenVerificationOptions struct {
AudienceClaims []string
ClientID string
ExtraAudiences []string
}

// NewVerifier constructs a new IDTokenVerifier
func NewVerifier(iv *oidc.IDTokenVerifier, vo *IDTokenVerificationOptions) *IDTokenVerifier {
allowedAudiences := make(map[string]struct{})
allowedAudiences[vo.ClientID] = struct{}{}
for _, extraAudience := range vo.ExtraAudiences {
allowedAudiences[extraAudience] = struct{}{}
}
return &IDTokenVerifier{iv, vo, allowedAudiences}
}

// Verify verifies incoming ID Token
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) {
token, err := v.IDTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("failed to verify token: %v", err)
}

claims := map[string]interface{}{}
if err := token.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
}

if isValidAudience, err := v.verifyAudience(token, claims); !isValidAudience {
return nil, err
}

return token, err
}

func (v *IDTokenVerifier) verifyAudience(token *oidc.IDToken, claims map[string]interface{}) (bool, error) {
for _, audienceClaim := range v.AudienceClaims {
if audienceClaimValue, audienceClaimExists := claims[audienceClaim]; audienceClaimExists {

// audience claim value can be either interface{} or []interface{},
// as per spec `aud` can be either a string or a list of strings
switch audienceClaimValueType := audienceClaimValue.(type) {
case []interface{}:
token.Audience = v.interfaceSliceToString(audienceClaimValue)
case interface{}:
token.Audience = []string{audienceClaimValue.(string)}
default:
return false, fmt.Errorf("audience claim %s holds unsupported type %T",
audienceClaim, audienceClaimValueType)
}

return v.isValidAudience(audienceClaim, token.Audience, v.allowedAudiences)
}
}

return false, fmt.Errorf("audience claims %v do not exist in claims: %v",
v.AudienceClaims, claims)
}

func (v *IDTokenVerifier) isValidAudience(claim string, audience []string, allowedAudiences map[string]struct{}) (bool, error) {
for _, aud := range audience {
if _, allowedAudienceExists := allowedAudiences[aud]; allowedAudienceExists {
return true, nil
}
}

return false, fmt.Errorf(
"audience from claim %s with value %s does not match with any of allowed audiences %v",
claim, audience, allowedAudiences)
}

func (v *IDTokenVerifier) interfaceSliceToString(slice interface{}) []string {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
panic(fmt.Sprintf("given a non-slice type %s", s.Kind()))
}
var strings []string
for i := 0; i < s.Len(); i++ {
strings = append(strings, s.Index(i).Interface().(string))
}
return strings
}
Loading

0 comments on commit 25371ea

Please sign in to comment.