forked from TrueCloudLab/certificates
Add support for validation of certificate requests using Azure subscription and AAD object IDs. See #735
This commit is contained in:
parent
c17886323a
commit
8b68bedffa
2 changed files with 90 additions and 13 deletions
|
@ -89,6 +89,8 @@ type Azure struct {
|
|||
Name string `json:"name"`
|
||||
TenantID string `json:"tenantID"`
|
||||
ResourceGroups []string `json:"resourceGroups"`
|
||||
SubscriptionIDs []string `json:"subscriptionIDs"`
|
||||
AadObjectIDs []string `json:"aadObjectIDs"`
|
||||
Audience string `json:"audience,omitempty"`
|
||||
DisableCustomSANs bool `json:"disableCustomSANs"`
|
||||
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
|
||||
|
@ -224,14 +226,14 @@ func (p *Azure) Init(config Config) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// authorizeToken returns the claims, name, group, error.
|
||||
func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) {
|
||||
// authorizeToken returns the claims, name, group, subscription, identityObjectID, error.
|
||||
func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, string, string, error) {
|
||||
jwt, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token")
|
||||
return nil, "", "", "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token")
|
||||
}
|
||||
if len(jwt.Headers) == 0 {
|
||||
return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header")
|
||||
return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; azure token missing header")
|
||||
}
|
||||
|
||||
var found bool
|
||||
|
@ -244,7 +246,7 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, "", "", errs.Unauthorized("azure.authorizeToken; cannot validate azure token")
|
||||
return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; cannot validate azure token")
|
||||
}
|
||||
|
||||
if err := claims.ValidateWithLeeway(jose.Expected{
|
||||
|
@ -252,26 +254,27 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, err
|
|||
Issuer: p.oidcConfig.Issuer,
|
||||
Time: time.Now(),
|
||||
}, 1*time.Minute); err != nil {
|
||||
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload")
|
||||
return nil, "", "", "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload")
|
||||
}
|
||||
|
||||
// Validate TenantID
|
||||
if claims.TenantID != p.TenantID {
|
||||
return nil, "", "", errs.Unauthorized("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)")
|
||||
return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)")
|
||||
}
|
||||
|
||||
re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID)
|
||||
if len(re) != 4 {
|
||||
return nil, "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID)
|
||||
return nil, "", "", "", "", errs.Unauthorized("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID)
|
||||
}
|
||||
group, name := re[2], re[3]
|
||||
return &claims, name, group, nil
|
||||
identityObjectID := claims.ObjectID
|
||||
subscription, group, name := re[1], re[2], re[3]
|
||||
return &claims, name, group, subscription, identityObjectID, nil
|
||||
}
|
||||
|
||||
// AuthorizeSign validates the given token and returns the sign options that
|
||||
// will be used on certificate creation.
|
||||
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||
_, name, group, err := p.authorizeToken(token)
|
||||
_, name, group, subscription, identityObjectID, err := p.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign")
|
||||
}
|
||||
|
@ -290,6 +293,34 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
}
|
||||
}
|
||||
|
||||
// Filter by subscription id
|
||||
if len(p.SubscriptionIDs) > 0 {
|
||||
var found bool
|
||||
for _, s := range p.SubscriptionIDs {
|
||||
if s == subscription {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errs.Unauthorized("azure.AuthorizeSign; azure token validation failed - invalid subscription id")
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Azure AD identity object id
|
||||
if len(p.AadObjectIDs) > 0 {
|
||||
var found bool
|
||||
for _, i := range p.AadObjectIDs {
|
||||
if i == identityObjectID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errs.Unauthorized("azure.AuthorizeSign; azure token validation failed - invalid identity object id")
|
||||
}
|
||||
}
|
||||
|
||||
// Template options
|
||||
data := x509util.NewTemplateData()
|
||||
data.SetCommonName(name)
|
||||
|
@ -348,7 +379,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
return nil, errs.Unauthorized("azure.AuthorizeSSHSign; sshCA is disabled for provisioner '%s'", p.GetName())
|
||||
}
|
||||
|
||||
_, name, _, err := p.authorizeToken(token)
|
||||
_, name, _, _, _, err := p.authorizeToken(token)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign")
|
||||
}
|
||||
|
|
|
@ -333,7 +333,7 @@ func TestAzure_authorizeToken(t *testing.T) {
|
|||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if claims, name, group, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||
if claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(errs.StatusCoder)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
||||
|
@ -348,6 +348,8 @@ func TestAzure_authorizeToken(t *testing.T) {
|
|||
|
||||
assert.Equals(t, name, "virtualMachine")
|
||||
assert.Equals(t, group, "resourceGroup")
|
||||
assert.Equals(t, subscriptionID, "subscriptionID")
|
||||
assert.Equals(t, objectID, "the-oid")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -382,6 +384,38 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
p4.oidcConfig = p1.oidcConfig
|
||||
p4.keyStore = p1.keyStore
|
||||
|
||||
p5, err := generateAzure()
|
||||
assert.FatalError(t, err)
|
||||
p5.TenantID = p1.TenantID
|
||||
p5.SubscriptionIDs = []string{"subscriptionID"}
|
||||
p5.config = p1.config
|
||||
p5.oidcConfig = p1.oidcConfig
|
||||
p5.keyStore = p1.keyStore
|
||||
|
||||
p6, err := generateAzure()
|
||||
assert.FatalError(t, err)
|
||||
p6.TenantID = p1.TenantID
|
||||
p6.SubscriptionIDs = []string{"foobarzar"}
|
||||
p6.config = p1.config
|
||||
p6.oidcConfig = p1.oidcConfig
|
||||
p6.keyStore = p1.keyStore
|
||||
|
||||
p7, err := generateAzure()
|
||||
assert.FatalError(t, err)
|
||||
p7.TenantID = p1.TenantID
|
||||
p7.AadObjectIDs = []string{"the-oid"}
|
||||
p7.config = p1.config
|
||||
p7.oidcConfig = p1.oidcConfig
|
||||
p7.keyStore = p1.keyStore
|
||||
|
||||
p8, err := generateAzure()
|
||||
assert.FatalError(t, err)
|
||||
p8.TenantID = p1.TenantID
|
||||
p8.AadObjectIDs = []string{"foobarzar"}
|
||||
p8.config = p1.config
|
||||
p8.oidcConfig = p1.oidcConfig
|
||||
p8.keyStore = p1.keyStore
|
||||
|
||||
badKey, err := generateJSONWebKey()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -393,6 +427,14 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
assert.FatalError(t, err)
|
||||
t4, err := p4.GetIdentityToken("subject", "caURL")
|
||||
assert.FatalError(t, err)
|
||||
t5, err := p5.GetIdentityToken("subject", "caURL")
|
||||
assert.FatalError(t, err)
|
||||
t6, err := p6.GetIdentityToken("subject", "caURL")
|
||||
assert.FatalError(t, err)
|
||||
t7, err := p6.GetIdentityToken("subject", "caURL")
|
||||
assert.FatalError(t, err)
|
||||
t8, err := p6.GetIdentityToken("subject", "caURL")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
t11, err := generateAzureToken("subject", p1.oidcConfig.Issuer, azureDefaultAudience,
|
||||
p1.TenantID, "subscriptionID", "resourceGroup", "virtualMachine",
|
||||
|
@ -434,8 +476,12 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
{"ok", p1, args{t1}, 5, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 10, http.StatusOK, false},
|
||||
{"ok", p1, args{t11}, 5, http.StatusOK, false},
|
||||
{"ok", p5, args{t5}, 5, http.StatusOK, false},
|
||||
{"ok", p7, args{t7}, 5, http.StatusOK, false},
|
||||
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
||||
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
||||
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
|
||||
{"fail object id", p8, args{t8}, 0, http.StatusUnauthorized, true},
|
||||
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
||||
{"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true},
|
||||
{"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true},
|
||||
|
|
Loading…
Reference in a new issue