forked from TrueCloudLab/certificates
Merge pull request #234 from smallstep/oidc-multinenant
Add support for multi-tenant OIDC provisioners
This commit is contained in:
commit
18869323f4
4 changed files with 49 additions and 29 deletions
|
@ -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"`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue