Skip to content

Commit

Permalink
Allow Bearer JWT client authentication
Browse files Browse the repository at this point in the history
This allows certificate based authentication without
exposing the private key to packer using an expiring
JWT containting the thumbprint (and sometimes the whole
certificate), signed using the certificate key pair.
  • Loading branch information
paulmey committed Oct 15, 2018
1 parent 4c1db9b commit 33f30a6
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 31 deletions.
64 changes: 50 additions & 14 deletions builder/azure/arm/authenticate.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
package arm

import (
"net/url"

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
)

type Authenticate struct {
env azure.Environment
clientID string
clientSecret string
tenantID string
type oAuthTokenProvider interface {
getServicePrincipalToken() (*adal.ServicePrincipalToken, error)
getServicePrincipalTokenWithResource(resource string) (*adal.ServicePrincipalToken, error)
}

func NewAuthenticate(env azure.Environment, clientID, clientSecret, tenantID string) *Authenticate {
return &Authenticate{
env: env,
clientID: clientID,
clientSecret: clientSecret,
tenantID: tenantID,
}
// for clientID/secret auth
type secretOAuthTokenProvider struct {
env azure.Environment
clientID, clientSecret, tenantID string
}

func (a *Authenticate) getServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
func NewSecretOAuthTokenProvider(env azure.Environment, clientID, clientSecret, tenantID string) oAuthTokenProvider {
return &secretOAuthTokenProvider{env, clientID, clientSecret, tenantID}
}

func (a *secretOAuthTokenProvider) getServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
return a.getServicePrincipalTokenWithResource(a.env.ResourceManagerEndpoint)
}

func (a *Authenticate) getServicePrincipalTokenWithResource(resource string) (*adal.ServicePrincipalToken, error) {
func (a *secretOAuthTokenProvider) getServicePrincipalTokenWithResource(resource string) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(a.env.ActiveDirectoryEndpoint, a.tenantID)
if err != nil {
return nil, err
Expand All @@ -39,3 +40,38 @@ func (a *Authenticate) getServicePrincipalTokenWithResource(resource string) (*a

return spt, err
}

// for clientID/bearer JWT auth
type jwtOAuthTokenProvider struct {
env azure.Environment
clientID, clientJWT, tenantID string
}

func NewJWTOAuthTokenProvider(env azure.Environment, clientID, clientJWT, tenantID string) oAuthTokenProvider {
return &jwtOAuthTokenProvider{env, clientID, clientJWT, tenantID}
}

func (pt *jwtOAuthTokenProvider) getServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
return pt.getServicePrincipalTokenWithResource(pt.env.ResourceManagerEndpoint)
}

func (tp *jwtOAuthTokenProvider) getServicePrincipalTokenWithResource(resource string) (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(tp.env.ActiveDirectoryEndpoint, tp.tenantID)
if err != nil {
return nil, err
}

return adal.NewServicePrincipalTokenWithSecret(
*oauthConfig,
tp.clientID,
resource,
tp)
}

// implements ServicePrincipalSecret
func (tp *jwtOAuthTokenProvider) SetAuthenticationValues(
t *adal.ServicePrincipalToken, v *url.Values) error {
v.Set("client_assertion", tp.clientJWT)
v.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
return nil
}
2 changes: 1 addition & 1 deletion builder/azure/arm/authenticate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// that cannot be done in a unit test because it involves network access. Instead,
// I assert the expected inertness of this class.
func TestNewAuthenticate(t *testing.T) {
testSubject := NewAuthenticate(azure.PublicCloud, "clientID", "clientString", "tenantID")
testSubject := NewSecretOAuthTokenProvider(azure.PublicCloud, "clientID", "clientString", "tenantID")
spn, err := testSubject.getServicePrincipalToken()
if err != nil {
t.Fatalf(err.Error())
Expand Down
70 changes: 55 additions & 15 deletions builder/azure/arm/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package arm
import (
"fmt"
"strings"
"time"

"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
jwt "github.com/dgrijalva/jwt-go"
packerAzureCommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/packer"
)
Expand All @@ -19,8 +21,12 @@ type ClientConfig struct {

// Authentication fields

ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
// Client ID
ClientID string `mapstructure:"client_id"`
// Client secret/password
ClientSecret string `mapstructure:"client_secret"`
// JWT bearer token for client auth (RFC 7523, Sec. 2.2)
ClientJWT string `mapstructure:"client_jwt"`
ObjectID string `mapstructure:"object_id"`
TenantID string `mapstructure:"tenant_id"`
SubscriptionID string `mapstructure:"subscription_id"`
Expand Down Expand Up @@ -81,17 +87,38 @@ func (c ClientConfig) assertRequiredParametersSet(errs *packer.MultiError) {
// readable by the ObjectID of the App. There may be another way to handle
// this case, but I am not currently aware of it - send feedback.

if !c.useDeviceLogin() {
if c.ClientID == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id must be specified"))
}
if c.SubscriptionID == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified"))
}

if c.ClientSecret == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_secret must be specified"))
}
if c.useDeviceLogin() {
// nothing else to check
return
}

if c.SubscriptionID == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified"))
if c.ClientID == "" ||
(c.ClientSecret == "" && c.ClientJWT == "") ||
(c.ClientSecret != "" && c.ClientJWT != "") {
// either client ID was not set, or neither or both secret and JWT are set
errs = packer.MultiErrorAppend(errs, fmt.Errorf("No valid set of authention methods specified: \n"+
"specify either (client_id,client_secret) or (client_id,client_jwt) to use a service principal, \n"+
"or specify none of these to use interactive user authentication."))
}

if c.ClientJWT != "" {
// should be a JWT that is valid for at least 5 more minutes
p := jwt.Parser{}
claims := jwt.StandardClaims{}
token, _, err := p.ParseUnverified(c.ClientJWT, &claims)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("client_jwt is not a JWT: %v", err))
} else {
if claims.ExpiresAt < time.Now().Add(5*time.Minute).Unix() {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("client_jwt will expire within 5 minutes, please use a JWT that is valid for at least 5 minutes"))
}
if t, ok := token.Header["x5t"]; !ok || t == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("client_jwt is missing the x5t header value, which is required for bearer JWT client authentication to Azure"))
}
}
}
}
Expand All @@ -100,6 +127,7 @@ func (c ClientConfig) useDeviceLogin() bool {
return c.SubscriptionID != "" &&
c.ClientID == "" &&
c.ClientSecret == "" &&
c.ClientJWT == "" &&
c.TenantID == ""
}

Expand All @@ -110,9 +138,6 @@ func (c ClientConfig) getServicePrincipalTokens(
err error) {

tenantID := c.TenantID
if tenantID == "" {
tenantID = "common"
}

if c.useDeviceLogin() {
say("Getting auth token for Service management endpoint")
Expand All @@ -126,8 +151,23 @@ func (c ClientConfig) getServicePrincipalTokens(
return nil, nil, err
}

} else if c.ClientSecret != "" {
say("Getting tokens using client secret")
auth := NewSecretOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientSecret, tenantID)

servicePrincipalToken, err = auth.getServicePrincipalToken()
if err != nil {
return nil, nil, err
}

servicePrincipalTokenVault, err = auth.getServicePrincipalTokenWithResource(
strings.TrimRight(c.cloudEnvironment.KeyVaultEndpoint, "/"))
if err != nil {
return nil, nil, err
}
} else {
auth := NewAuthenticate(*c.cloudEnvironment, c.ClientID, c.ClientSecret, tenantID)
say("Getting tokens using client bearer JWT")
auth := NewJWTOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientJWT, tenantID)

servicePrincipalToken, err = auth.getServicePrincipalToken()
if err != nil {
Expand Down
Loading

0 comments on commit 33f30a6

Please sign in to comment.