diff --git a/pkg/apis/middleware/session.go b/pkg/apis/middleware/session.go index 9fcd974b..a7d32014 100644 --- a/pkg/apis/middleware/session.go +++ b/pkg/apis/middleware/session.go @@ -25,6 +25,7 @@ func CreateTokenToSessionFunc(verify VerifyFunc) TokenToSessionFunc { Verified *bool `json:"email_verified"` PreferredUsername string `json:"preferred_username"` Groups []string `json:"groups"` + ACR string `json:"acr"` } idToken, err := verify(ctx, token) @@ -49,6 +50,7 @@ func CreateTokenToSessionFunc(verify VerifyFunc) TokenToSessionFunc { User: claims.Subject, Groups: claims.Groups, PreferredUsername: claims.PreferredUsername, + ACR: claims.ACR, AccessToken: token, IDToken: token, RefreshToken: "", diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index 0f254575..ecdee402 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -272,6 +272,8 @@ type OIDCOptions struct { // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. ExtraAudiences []string `json:"extraAudiences,omitempty"` + // to pass acr values to the provider + ACRs string `json:"acr,omitempty"` } type LoginGovOptions struct { diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 5b063c3f..79d4f01f 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -27,6 +27,7 @@ type SessionState struct { User string `msgpack:"u,omitempty"` Groups []string `msgpack:"g,omitempty"` PreferredUsername string `msgpack:"pu,omitempty"` + ACR string `msgpack:"acr,omitempty"` // Internal helpers, not serialized Clock func() time.Time `msgpack:"-"` // override for time.Now, for testing @@ -154,6 +155,8 @@ func (s *SessionState) GetClaim(claim string) []string { return groups case "preferred_username": return []string{s.PreferredUsername} + case "acr": + return []string{s.ACR} default: return []string{} } diff --git a/providers/oidc.go b/providers/oidc.go index 15598aba..37c43b8e 100644 --- a/providers/oidc.go +++ b/providers/oidc.go @@ -46,6 +46,7 @@ func NewOIDCProvider(p *ProviderData, opts options.OIDCOptions) *OIDCProvider { } p.setProviderDefaults(oidcProviderDefaults) + p.setAllowedACR(opts.ACRs) p.getAuthorizationHeaderFunc = makeOIDCHeader return &OIDCProvider{ diff --git a/providers/provider_data.go b/providers/provider_data.go index 95de5c50..99283478 100644 --- a/providers/provider_data.go +++ b/providers/provider_data.go @@ -22,6 +22,7 @@ import ( const ( // This is not exported as it's not currently user configurable oidcUserClaim = "sub" + oidcAcrClaim = "acr" ) // ProviderData contains information required to configure all implementations @@ -55,6 +56,7 @@ type ProviderData struct { // Universal Group authorization data structure // any provider can set to consume AllowedGroups map[string]struct{} + AllowedACRs map[string]struct{} getAuthorizationHeaderFunc func(string) http.Header loginURLParameterDefaults url.Values @@ -183,6 +185,14 @@ func (p *ProviderData) setAllowedGroups(groups []string) { } } +func (p *ProviderData) setAllowedACR(acrs string) { + p.AllowedACRs = make(map[string]struct{}) + var resultingACRs = strings.Split(acrs, ",") + for _, acr := range resultingACRs { + p.AllowedACRs[acr] = struct{}{} + } +} + type providerDefaults struct { name string loginURL *url.URL @@ -260,6 +270,7 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (* {p.UserClaim, &ss.User}, {p.EmailClaim, &ss.Email}, {p.GroupsClaim, &ss.Groups}, + {oidcAcrClaim, &ss.ACR}, // TODO (@NickMeves) Deprecate for dynamic claim to session mapping {"preferred_username", &ss.PreferredUsername}, } { diff --git a/providers/provider_default.go b/providers/provider_default.go index 1735ddd3..50bcdac9 100644 --- a/providers/provider_default.go +++ b/providers/provider_default.go @@ -121,6 +121,12 @@ func (p *ProviderData) EnrichSession(_ context.Context, _ *sessions.SessionState // Authorize performs global authorization on an authenticated session. // This is not used for fine-grained per route authorization rules. func (p *ProviderData) Authorize(_ context.Context, s *sessions.SessionState) (bool, error) { + if len(p.AllowedACRs) > 0 { + if _, ok := p.AllowedACRs[s.ACR]; !ok { + return false, nil + } + } + if len(p.AllowedGroups) == 0 { return true, nil } diff --git a/providers/provider_default_test.go b/providers/provider_default_test.go index 0fbe7abd..7b4a8a05 100644 --- a/providers/provider_default_test.go +++ b/providers/provider_default_test.go @@ -76,6 +76,8 @@ func TestProviderDataAuthorize(t *testing.T) { name string allowedGroups []string groups []string + acr string + userAcr string expectedAuthZ bool }{ { @@ -102,6 +104,23 @@ func TestProviderDataAuthorize(t *testing.T) { groups: []string{"baz", "foo"}, expectedAuthZ: false, }, + { + name: "UserNotAllowedForACRLevel", + acr: "1", + expectedAuthZ: false, + }, + { + name: "UserNotAllowedForACRLevel", + acr: "1", + userAcr: "1", + expectedAuthZ: true, + }, + { + name: "UserNotAllowedForACRLevel", + acr: "2", + userAcr: "somethingElse", + expectedAuthZ: false, + }, } for _, tc := range testCases { @@ -110,9 +129,11 @@ func TestProviderDataAuthorize(t *testing.T) { session := &sessions.SessionState{ Groups: tc.groups, + ACR: tc.userAcr, } p := &ProviderData{} p.setAllowedGroups(tc.allowedGroups) + p.setAllowedACR(tc.acr) authorized, err := p.Authorize(context.Background(), session) g.Expect(err).ToNot(HaveOccurred())