Merge pull request #234 from smallstep/oidc-multinenant

Add support for multi-tenant OIDC provisioners
This commit is contained in:
Mariano Cano 2020-04-27 15:21:55 -07:00 committed by GitHub
commit 18869323f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 29 deletions

View file

@ -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"`

View file

@ -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 {

View file

@ -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)
}
})
}

View file

@ -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")