Skip to content

Commit

Permalink
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
Browse files Browse the repository at this point in the history
NOTE: this is /intentionally/ feature-toggled off for now
tombuildsstuff committed Nov 2, 2018

Verified

This commit was signed with the committer’s verified signature.
cmmarslender Chris Marslender
1 parent 3a60e48 commit 203a0d3
Showing 3 changed files with 301 additions and 0 deletions.
115 changes: 115 additions & 0 deletions azurerm/helpers/authentication/auth_method_client_cert.go
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 azurerm/helpers/authentication/auth_method_client_cert_test.go
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)
}
}
}
13 changes: 13 additions & 0 deletions azurerm/helpers/authentication/builder.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,11 @@ type Builder struct {
SupportsManagedServiceIdentity bool
MsiEndpoint string

// Service Principal (Client Cert) Auth
SupportsClientCertAuth bool
ClientCertPath string
ClientCertPassword string

// Service Principal (Client Secret) Auth
SupportsClientSecretAuth bool
ClientSecret string
@@ -32,6 +37,14 @@ func (b Builder) Build() (*Config, error) {
Environment: b.Environment,
}

if b.SupportsClientCertAuth && b.ClientCertPath != "" {
log.Printf("[DEBUG] Using Service Principal / Client Certificate for Authentication")
config.AuthenticatedAsAServicePrincipal = true

config.authMethod = newServicePrincipalClientCertificateAuth(b)
return config.validate()
}

if b.SupportsClientSecretAuth && b.ClientSecret != "" {
log.Printf("[DEBUG] Using Service Principal / Client Secret for Authentication")
config.AuthenticatedAsAServicePrincipal = true

0 comments on commit 203a0d3

Please sign in to comment.