-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New authentication method: Service Principal Client Certificate
NOTE: this is /intentionally/ feature-toggled off for now
1 parent
3a60e48
commit 203a0d3
Showing
3 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
115 changes: 115 additions & 0 deletions
115
azurerm/helpers/authentication/auth_method_client_cert.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package authentication | ||
|
||
import ( | ||
"crypto/rsa" | ||
"crypto/x509" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"strings" | ||
|
||
"github.com/Azure/go-autorest/autorest" | ||
"github.com/Azure/go-autorest/autorest/adal" | ||
"github.com/hashicorp/go-multierror" | ||
"golang.org/x/crypto/pkcs12" | ||
) | ||
|
||
type servicePrincipalClientCertificateAuth struct { | ||
clientId string | ||
clientCertPath string | ||
clientCertPassword string | ||
environment string | ||
subscriptionId string | ||
tenantId string | ||
} | ||
|
||
func newServicePrincipalClientCertificateAuth(b Builder) authMethod { | ||
return servicePrincipalClientCertificateAuth{ | ||
clientId: b.ClientID, | ||
clientCertPath: b.ClientCertPath, | ||
clientCertPassword: b.ClientCertPassword, | ||
environment: b.Environment, | ||
subscriptionId: b.SubscriptionID, | ||
tenantId: b.TenantID, | ||
} | ||
} | ||
|
||
func (a servicePrincipalClientCertificateAuth) getAuthorizationToken(oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { | ||
certificateData, err := ioutil.ReadFile(a.clientCertPath) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error reading Client Certificate %q: %v", a.clientCertPath, err) | ||
} | ||
|
||
// Get the certificate and private key from pfx file | ||
certificate, rsaPrivateKey, err := decodePkcs12(certificateData, a.clientCertPassword) | ||
if err != nil { | ||
return nil, fmt.Errorf("Error decoding pkcs12 certificate: %v", err) | ||
} | ||
|
||
spt, err := adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, a.clientId, certificate, rsaPrivateKey, endpoint) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = spt.Refresh() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
auth := autorest.NewBearerAuthorizer(spt) | ||
return auth, nil | ||
} | ||
|
||
func (a servicePrincipalClientCertificateAuth) validate() error { | ||
var err *multierror.Error | ||
|
||
fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Client Certificate." | ||
|
||
if a.subscriptionId == "" { | ||
err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) | ||
} | ||
|
||
if a.clientId == "" { | ||
err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) | ||
} | ||
|
||
if a.clientCertPath == "" { | ||
err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Certificate Path")) | ||
} else { | ||
if strings.HasSuffix(strings.ToLower(a.clientCertPath), ".pfx") { | ||
// ensure it exists on disk | ||
_, fileErr := os.Stat(a.clientCertPath) | ||
if os.IsNotExist(fileErr) { | ||
err = multierror.Append(err, fmt.Errorf("Error locating Client Certificate specified at %q: %s", a.clientCertPath, fileErr)) | ||
} | ||
|
||
// we're intentionally /not/ checking it's an actual PFX file at this point, as that happens in the getAuthorizationToken | ||
} else { | ||
err = multierror.Append(err, fmt.Errorf("The Client Certificate Path is not a *.pfx file: %q", a.clientCertPath)) | ||
} | ||
} | ||
|
||
if a.tenantId == "" { | ||
err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) | ||
} | ||
|
||
if a.environment == "" { | ||
err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Environment")) | ||
} | ||
|
||
return err.ErrorOrNil() | ||
} | ||
|
||
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) { | ||
privateKey, certificate, err := pkcs12.Decode(pkcs, password) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey) | ||
if !isRsaKey { | ||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key") | ||
} | ||
|
||
return certificate, rsaPrivateKey, nil | ||
} |
173 changes: 173 additions & 0 deletions
173
azurerm/helpers/authentication/auth_method_client_cert_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package authentication | ||
|
||
import ( | ||
"io/ioutil" | ||
"os" | ||
"testing" | ||
) | ||
|
||
func TestServicePrincipalClientCertAuth_builder(t *testing.T) { | ||
builder := Builder{ | ||
ClientID: "some-client-id", | ||
ClientCertPath: "some-client-cert-path", | ||
ClientCertPassword: "some-password", | ||
Environment: "some-environment", | ||
SubscriptionID: "some-subscription-id", | ||
TenantID: "some-tenant-id", | ||
} | ||
config := newServicePrincipalClientCertificateAuth(builder) | ||
servicePrincipal := config.(servicePrincipalClientCertificateAuth) | ||
|
||
if builder.ClientID != servicePrincipal.clientId { | ||
t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) | ||
} | ||
|
||
if builder.ClientCertPath != servicePrincipal.clientCertPath { | ||
t.Fatalf("Expected Client Certificate Path to be %q but got %q", builder.ClientCertPath, servicePrincipal.clientCertPath) | ||
} | ||
|
||
if builder.ClientCertPassword != servicePrincipal.clientCertPassword { | ||
t.Fatalf("Expected Client Certificate Password to be %q but got %q", builder.ClientCertPassword, servicePrincipal.clientCertPassword) | ||
} | ||
|
||
if builder.Environment != servicePrincipal.environment { | ||
t.Fatalf("Expected Environment to be %q but got %q", builder.Environment, servicePrincipal.environment) | ||
} | ||
|
||
if builder.SubscriptionID != servicePrincipal.subscriptionId { | ||
t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) | ||
} | ||
|
||
if builder.TenantID != servicePrincipal.tenantId { | ||
t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) | ||
} | ||
} | ||
|
||
func TestServicePrincipalClientCertAuth_validate(t *testing.T) { | ||
data := []byte("client-cert-auth") | ||
filePath := "./example.pfx" | ||
err := ioutil.WriteFile(filePath, data, 0600) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.Remove(filePath) | ||
|
||
cases := []struct { | ||
Description string | ||
Config servicePrincipalClientCertificateAuth | ||
ExpectError bool | ||
}{ | ||
{ | ||
Description: "Empty Configuration", | ||
Config: servicePrincipalClientCertificateAuth{}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Missing Client ID", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: filePath, | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Missing Subscription ID", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
clientCertPath: filePath, | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Missing Client Certificate Path", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Missing Tenant ID", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: filePath, | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Missing Environment", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: filePath, | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "File isn't a pfx", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: "not-valid.txt", | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "File does not exist", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: "does-not-exist.pfx", | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: true, | ||
}, | ||
{ | ||
Description: "Valid Configuration (Basic)", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: filePath, | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: false, | ||
}, | ||
{ | ||
Description: "Valid Configuration (Complete)", | ||
Config: servicePrincipalClientCertificateAuth{ | ||
clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", | ||
subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", | ||
clientCertPath: filePath, | ||
clientCertPassword: "Password1234!", | ||
tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", | ||
environment: "public", | ||
}, | ||
ExpectError: false, | ||
}, | ||
} | ||
|
||
for _, v := range cases { | ||
err := v.Config.validate() | ||
|
||
if v.ExpectError && err == nil { | ||
t.Fatalf("Expected an error for %q: didn't get one", v.Description) | ||
} | ||
|
||
if !v.ExpectError && err != nil { | ||
t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters