From 4e9bff0986d117e1fa83652338796a3b490ce0c9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 24 Apr 2020 14:36:32 -0700 Subject: [PATCH] Add support for OIDC multitoken tenants for azure. --- authority/provisioner/azure.go | 2 +- authority/provisioner/oidc.go | 5 +++ authority/provisioner/oidc_test.go | 69 +++++++++++++++++------------ authority/provisioner/utils_test.go | 2 + 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 5afe0472..cd1b1b83 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -84,7 +84,7 @@ type Azure struct { *base Type string `json:"type"` Name string `json:"name"` - TenantID string `json:"tenantId"` + TenantID string `json:"tenantID"` ResourceGroups []string `json:"resourceGroups"` Audience string `json:"audience,omitempty"` DisableCustomSANs bool `json:"disableCustomSANs"` diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index f90d96b5..e675c503 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -57,6 +57,7 @@ type OIDC struct { ClientID string `json:"clientID"` ClientSecret string `json:"clientSecret"` ConfigurationEndpoint string `json:"configurationEndpoint"` + TenantID string `json:"tenantID,omitempty"` Admins []string `json:"admins,omitempty"` Domains []string `json:"domains,omitempty"` Groups []string `json:"groups,omitempty"` @@ -166,6 +167,10 @@ func (o *OIDC) Init(config Config) (err error) { if err := o.configuration.Validate(); err != nil { return errors.Wrapf(err, "error parsing %s", o.ConfigurationEndpoint) } + // Replace {tenantid} with the configured one + if o.TenantID != "" { + o.configuration.Issuer = strings.Replace(o.configuration.Issuer, "{tenantid}", o.TenantID, -1) + } // Get JWK key set o.keyStore, err = newKeyStore(o.configuration.JWKSetURI) if err != nil { diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index fbf71f4b..c879a66b 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -139,12 +139,16 @@ func TestOIDC_Init(t *testing.T) { } func TestOIDC_authorizeToken(t *testing.T) { - srv := generateJWKServer(2) + srv := generateJWKServer(3) defer srv.Close() var keys jose.JSONWebKeySet assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys)) + issuer := "the-issuer" + tenantID := "ab800f7d-2c87-45fb-b1d0-f90d0bc5ec25" + tenantIssuer := "https://login.microsoftonline.com/" + tenantID + "/v2.0" + // Create test provisioners p1, err := generateOIDC() assert.FatalError(t, err) @@ -152,6 +156,8 @@ func TestOIDC_authorizeToken(t *testing.T) { assert.FatalError(t, err) p3, err := generateOIDC() assert.FatalError(t, err) + // TenantID + p2.TenantID = tenantID // Admin + Domains p3.Admins = []string{"name@smallstep.com", "root@example.com"} p3.Domains = []string{"smallstep.com"} @@ -159,20 +165,24 @@ func TestOIDC_authorizeToken(t *testing.T) { // Update configuration endpoints and initialize config := Config{Claims: globalProvisionerClaims} p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" - p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" + p2.ConfigurationEndpoint = srv.URL + "/common/.well-known/openid-configuration" p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration" assert.FatalError(t, p1.Init(config)) assert.FatalError(t, p2.Init(config)) assert.FatalError(t, p3.Init(config)) - t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0]) + t1, err := generateSimpleToken(issuer, p1.ClientID, &keys.Keys[0]) assert.FatalError(t, err) - t2, err := generateSimpleToken("the-issuer", p2.ClientID, &keys.Keys[1]) + t2, err := generateSimpleToken(tenantIssuer, p2.ClientID, &keys.Keys[1]) + assert.FatalError(t, err) + t3, err := generateToken("subject", issuer, p3.ClientID, "name@smallstep.com", []string{}, time.Now(), &keys.Keys[2]) + assert.FatalError(t, err) + t4, err := generateToken("subject", issuer, p3.ClientID, "foo@smallstep.com", []string{}, time.Now(), &keys.Keys[2]) assert.FatalError(t, err) // Invalid email - failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0]) + failEmail, err := generateToken("subject", issuer, p3.ClientID, "", []string{}, time.Now(), &keys.Keys[2]) assert.FatalError(t, err) - failDomain, err := generateToken("subject", "the-issuer", p3.ClientID, "name@example.com", []string{}, time.Now(), &keys.Keys[0]) + failDomain, err := generateToken("subject", issuer, p3.ClientID, "name@example.com", []string{}, time.Now(), &keys.Keys[2]) assert.FatalError(t, err) // Invalid tokens @@ -180,7 +190,7 @@ func TestOIDC_authorizeToken(t *testing.T) { key, err := generateJSONWebKey() assert.FatalError(t, err) // missing key - failKey, err := generateSimpleToken("the-issuer", p1.ClientID, key) + failKey, err := generateSimpleToken(issuer, p1.ClientID, key) assert.FatalError(t, err) // invalid token failTok := "foo." + parts[1] + "." + parts[2] @@ -190,39 +200,42 @@ func TestOIDC_authorizeToken(t *testing.T) { failIss, err := generateSimpleToken("bad-issuer", p1.ClientID, &keys.Keys[0]) assert.FatalError(t, err) // invalid audience - failAud, err := generateSimpleToken("the-issuer", "foobar", &keys.Keys[0]) + failAud, err := generateSimpleToken(issuer, "foobar", &keys.Keys[0]) assert.FatalError(t, err) // invalid signature failSig := t1[0 : len(t1)-2] // expired - failExp, err := generateToken("subject", "the-issuer", p1.ClientID, "name@smallstep.com", []string{}, time.Now().Add(-360*time.Second), &keys.Keys[0]) + failExp, err := generateToken("subject", issuer, p1.ClientID, "name@smallstep.com", []string{}, time.Now().Add(-360*time.Second), &keys.Keys[0]) assert.FatalError(t, err) // not before - failNbf, err := generateToken("subject", "the-issuer", p1.ClientID, "name@smallstep.com", []string{}, time.Now().Add(360*time.Second), &keys.Keys[0]) + failNbf, err := generateToken("subject", issuer, p1.ClientID, "name@smallstep.com", []string{}, time.Now().Add(360*time.Second), &keys.Keys[0]) assert.FatalError(t, err) type args struct { token string } tests := []struct { - name string - prov *OIDC - args args - code int - wantErr bool + name string + prov *OIDC + args args + code int + wantIssuer string + wantErr bool }{ - {"ok1", p1, args{t1}, http.StatusOK, false}, - {"ok2", p2, args{t2}, http.StatusOK, false}, - {"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true}, - {"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, true}, - {"fail-key", p1, args{failKey}, http.StatusUnauthorized, true}, - {"fail-token", p1, args{failTok}, http.StatusUnauthorized, true}, - {"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, true}, - {"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, true}, - {"fail-audience", p1, args{failAud}, http.StatusUnauthorized, true}, - {"fail-signature", p1, args{failSig}, http.StatusUnauthorized, true}, - {"fail-expired", p1, args{failExp}, http.StatusUnauthorized, true}, - {"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, true}, + {"ok1", p1, args{t1}, http.StatusOK, issuer, false}, + {"ok tenantid", p2, args{t2}, http.StatusOK, tenantIssuer, false}, + {"ok admin", p3, args{t3}, http.StatusOK, issuer, false}, + {"ok domain", p3, args{t4}, http.StatusOK, issuer, false}, + {"fail-email", p3, args{failEmail}, http.StatusUnauthorized, "", true}, + {"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, "", true}, + {"fail-key", p1, args{failKey}, http.StatusUnauthorized, "", true}, + {"fail-token", p1, args{failTok}, http.StatusUnauthorized, "", true}, + {"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, "", true}, + {"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, "", true}, + {"fail-audience", p1, args{failAud}, http.StatusUnauthorized, "", true}, + {"fail-signature", p1, args{failSig}, http.StatusUnauthorized, "", true}, + {"fail-expired", p1, args{failExp}, http.StatusUnauthorized, "", true}, + {"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -239,7 +252,7 @@ func TestOIDC_authorizeToken(t *testing.T) { assert.Nil(t, got) } else { assert.NotNil(t, got) - assert.Equals(t, got.Issuer, "the-issuer") + assert.Equals(t, got.Issuer, tt.wantIssuer) } }) } diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 7d200d33..418e6a5b 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -943,6 +943,8 @@ func generateJWKServer(n int) *httptest.Server { writeJSON(w, hits) case "/.well-known/openid-configuration": writeJSON(w, openIDConfiguration{Issuer: "the-issuer", JWKSetURI: srv.URL + "/jwks_uri"}) + case "/common/.well-known/openid-configuration": + writeJSON(w, openIDConfiguration{Issuer: "https://login.microsoftonline.com/{tenantid}/v2.0", JWKSetURI: srv.URL + "/jwks_uri"}) case "/random": keySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet) w.Header().Add("Cache-Control", "max-age=5")