Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for using System Assigned Managed Identities on Azure #37

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
35 changes: 18 additions & 17 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,24 @@ import (
)

type Config struct {
ApplicationId string // The Application Id that the Azure app registration portal assigned when the app was registered.
Authority string // The Azure ActiveDirectory Authority URL
ClientSecret string // The Application Secret that was generated for the app in the app registration portal.
ClientCert string // The certificate uploaded to the app registration portal."
ClientKey string // The key for a certificate uploaded to the app registration portal."
ClientKeyPass string // The passphrase to use in conjuction with the associated key of a certificate uploaded to the app registration portal."
Graph string // The Microsoft Graph URL
JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs
Management string // The Azure ResourceManager URL
MgmtGroupId []string // The Management Group Id to use as a filter
Password string // The password associated with the user principal name associated with the Azure portal.
ProxyUrl string // The forward proxy url
RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs
Region string // The region of the Azure Cloud deployment.
SubscriptionId []string // The Subscription Id(s) to use as a filter
Tenant string // The directory tenant that you want to request permission from. This can be in GUID or friendly name format
Username string // The user principal name associated with the Azure portal.
ApplicationId string // The Application Id that the Azure app registration portal assigned when the app was registered.
Authority string // The Azure ActiveDirectory Authority URL
ClientSecret string // The Application Secret that was generated for the app in the app registration portal.
ClientCert string // The certificate uploaded to the app registration portal."
ClientKey string // The key for a certificate uploaded to the app registration portal."
ClientKeyPass string // The passphrase to use in conjuction with the associated key of a certificate uploaded to the app registration portal."
Graph string // The Microsoft Graph URL
JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs
Management string // The Azure ResourceManager URL
MgmtGroupId []string // The Management Group Id to use as a filter
Password string // The password associated with the user principal name associated with the Azure portal.
ProxyUrl string // The forward proxy url
RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs
Region string // The region of the Azure Cloud deployment.
SubscriptionId []string // The Subscription Id(s) to use as a filter
Tenant string // The directory tenant that you want to request permission from. This can be in GUID or friendly name format
Username string // The user principal name associated with the Azure portal.
SystemAssignedId bool // Use of a system assigned managed identity instead of a user provided identity.
}

func AuthorityUrl(region string, defaultUrl string) string {
Expand Down
115 changes: 71 additions & 44 deletions client/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,31 @@ func NewRestClient(apiUrl string, config config.Config) (RestClient, error) {
Token{},
config.SubscriptionId,
config.MgmtGroupId,
config.SystemAssignedId,
}
return client, nil
}
}

type restClient struct {
api url.URL
authUrl url.URL
jwt string
clientId string
clientSecret string
clientCert string
clientKey string
clientKeyPass string
username string
password string
http *http.Client
mutex sync.RWMutex
refreshToken string
tenant string
token Token
subId []string
mgmtGroupId []string
api url.URL
authUrl url.URL
jwt string
clientId string
clientSecret string
clientCert string
clientKey string
clientKeyPass string
username string
password string
http *http.Client
mutex sync.RWMutex
refreshToken string
tenant string
token Token
subId []string
mgmtGroupId []string
systemAssignedId bool
}

func (s *restClient) Authenticate() error {
Expand All @@ -106,41 +108,66 @@ func (s *restClient) Authenticate() error {
body = url.Values{}
)

if s.clientId == "" {
body.Add("client_id", constants.AzPowerShellClientID)
} else {
body.Add("client_id", s.clientId)
}
if s.systemAssignedId {

body.Add("scope", scope.ResolveReference(&defaultScope).String())
endpoint, _ = url.Parse("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01")
scope = &s.api // Don't use the .default after the API, since that's a scope not a resource
getArgs := endpoint.Query()
getArgs.Add("resource", scope.String())
endpoint.RawQuery = getArgs.Encode()

if s.refreshToken != "" {
body.Add("grant_type", "refresh_token")
body.Add("refresh_token", s.refreshToken)
body.Set("client_id", constants.AzPowerShellClientID)
} else if s.clientSecret != "" {
body.Add("grant_type", "client_credentials")
body.Add("client_secret", s.clientSecret)
} else if s.clientCert != "" && s.clientKey != "" {
if clientAssertion, err := NewClientAssertion(endpoint.String(), s.clientId, s.clientCert, s.clientKey, s.clientKeyPass); err != nil {
return err
} else {

if s.clientId == "" {
body.Add("client_id", constants.AzPowerShellClientID)
} else {
body.Add("client_id", s.clientId)
}

body.Add("scope", scope.ResolveReference(&defaultScope).String())

if s.refreshToken != "" {
body.Add("grant_type", "refresh_token")
body.Add("refresh_token", s.refreshToken)
body.Set("client_id", constants.AzPowerShellClientID)
} else if s.clientSecret != "" {
body.Add("grant_type", "client_credentials")
body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
body.Add("client_assertion", clientAssertion)
body.Add("client_secret", s.clientSecret)
} else if s.clientCert != "" && s.clientKey != "" {
if clientAssertion, err := NewClientAssertion(endpoint.String(), s.clientId, s.clientCert, s.clientKey, s.clientKeyPass); err != nil {
return err
} else {
body.Add("grant_type", "client_credentials")
body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
body.Add("client_assertion", clientAssertion)
}
} else if s.username != "" && s.password != "" {
body.Add("grant_type", "password")
body.Add("username", s.username)
body.Add("password", s.password)
body.Set("client_id", constants.AzPowerShellClientID)
} else {
return fmt.Errorf("unable to authenticate. no valid credential provided")
}
}

var req *http.Request
var err error
if s.systemAssignedId {
req, err = NewRequest(context.Background(), "GET", endpoint, nil, nil, nil)
if err != nil {
return err
}
} else if s.username != "" && s.password != "" {
body.Add("grant_type", "password")
body.Add("username", s.username)
body.Add("password", s.password)
body.Set("client_id", constants.AzPowerShellClientID)
req.Header.Add("Metadata", "true")
} else {
return fmt.Errorf("unable to authenticate. no valid credential provided")
req, err = NewRequest(context.Background(), "POST", endpoint, body, nil, nil)
if err != nil {
return err
}

}

if req, err := NewRequest(context.Background(), "POST", endpoint, body, nil, nil); err != nil {
return err
} else if res, err := s.send(req); err != nil {
if res, err := s.send(req); err != nil {
return err
} else {
defer res.Body.Close()
Expand Down
42 changes: 31 additions & 11 deletions client/rest/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ package rest
import (
"encoding/json"
"fmt"
"strconv"
"time"
)

type Token struct {
accessToken string
expiresIn int
extExpiresIn int
expires time.Time
accessToken string
expiresIn int
expires time.Time
}

func (s Token) IsExpired() bool {
Expand All @@ -38,21 +38,41 @@ func (s Token) String() string {
return fmt.Sprintf("Bearer %s", s.accessToken)
}

// The code below uses this weird unmarshalling way for ExpiresIn and ExtExpiresIn
// because the metadata APIs used for obtaining a System Assigned Managed Identity return integer values which are quoted (i.e. {"expires_in" : "86400"})
// while the normal 'login.microsoft.com' APIs return expires_in as a proper integer (i.e. {"expires_in" : 86400}). The previous code was failing
// to parse the response of the metadata APIs since it was a string and not an int. This solves it for both.
// Another change is to remove the "ExtExpiresIn" since this field is not present for (system assigned) managed identities. As this field is not used
// throughout the code anyway, we can remove it for now. If this field is not present on Windows, everything keeps working without any error. On linux/docker
// this breaks the authentication flow.
func (s *Token) UnmarshalJSON(data []byte) error {
var res struct {
AccessToken string `json:"access_token"` // The token to use in calls to Microsoft Graph API
ExpiresIn int `json:"expires_in"` // How long the access token is valid in seconds
ExtExpiresIn int `json:"ext_expires_in"` // How long the access token is valid in seconds
TokenType string `json:"token_type"` // Indicates the token type value. The only type currently supported by Azure AD is `bearer`
AccessToken string `json:"access_token"` // The token to use in calls to Microsoft Graph API
ExpiresIn interface{} `json:"expires_in"` // How long the access token is valid in seconds
TokenType string `json:"token_type"` // Indicates the token type value. The only type currently supported by Azure AD is `bearer`
}

if err := json.Unmarshal(data, &res); err != nil {
return err
} else {
// convert ExpiresIn to int
expiresIn, ok := res.ExpiresIn.(int)
if !ok {
// ExpiresIn is not an int
// try to convert it from string to int
str, ok := res.ExpiresIn.(string)
if !ok {
return nil
}
expiresIn, err = strconv.Atoi(str)
if err != nil {
return nil
}
}

s.expiresIn = expiresIn
s.accessToken = res.AccessToken
s.expiresIn = res.ExpiresIn
s.extExpiresIn = res.ExtExpiresIn
s.expires = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second)
s.expires = time.Now().Add(time.Duration(expiresIn) * time.Second)
return nil
}
}
35 changes: 18 additions & 17 deletions cmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,23 +236,24 @@ func newAzureClient() (client.AzureClient, error) {
}

config := client_config.Config{
ApplicationId: config.AzAppId.Value().(string),
Authority: config.AzAuthUrl.Value().(string),
ClientSecret: config.AzSecret.Value().(string),
ClientCert: clientCert,
ClientKey: clientKey,
ClientKeyPass: config.AzKeyPass.Value().(string),
Graph: config.AzGraphUrl.Value().(string),
JWT: config.JWT.Value().(string),
Management: config.AzMgmtUrl.Value().(string),
MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
Password: config.AzPassword.Value().(string),
ProxyUrl: config.Proxy.Value().(string),
RefreshToken: config.RefreshToken.Value().(string),
Region: config.AzRegion.Value().(string),
SubscriptionId: config.AzSubId.Value().([]string),
Tenant: config.AzTenant.Value().(string),
Username: config.AzUsername.Value().(string),
ApplicationId: config.AzAppId.Value().(string),
Authority: config.AzAuthUrl.Value().(string),
ClientSecret: config.AzSecret.Value().(string),
ClientCert: clientCert,
ClientKey: clientKey,
ClientKeyPass: config.AzKeyPass.Value().(string),
Graph: config.AzGraphUrl.Value().(string),
JWT: config.JWT.Value().(string),
Management: config.AzMgmtUrl.Value().(string),
MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
Password: config.AzPassword.Value().(string),
ProxyUrl: config.Proxy.Value().(string),
RefreshToken: config.RefreshToken.Value().(string),
Region: config.AzRegion.Value().(string),
SubscriptionId: config.AzSubId.Value().([]string),
Tenant: config.AzTenant.Value().(string),
Username: config.AzUsername.Value().(string),
SystemAssignedId: config.SystemAssignedId.Value().(bool),
}
return client.NewClient(config)
}
Expand Down
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ var (
Persistent: true,
Default: "",
}
SystemAssignedId = Config{
Name: "system-id",
Shorthand: "",
Usage: "Use a System Assigned Managed Identity",
Persistent: true,
Default: false,
}

// Azure Configurations
AzAppId = Config{
Expand Down Expand Up @@ -283,6 +290,7 @@ var (
LogFile,
Proxy,
RefreshToken,
SystemAssignedId,
}

AzureConfig = []Config{
Expand Down
8 changes: 5 additions & 3 deletions enums/auth-method.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ package enums
type AuthMethod = string

const (
Certificate string = "Certificate"
Secret string = "Client Secret"
UsernamePassword string = "Username and Password"
Certificate string = "Certificate"
Secret string = "Client Secret"
UsernamePassword string = "Username and Password"
SystemAssignedManagedIdentity string = "System Assigned Managed Identity"
)

func AuthMethods() []AuthMethod {
return []AuthMethod{
Certificate,
Secret,
UsernamePassword,
SystemAssignedManagedIdentity,
}
}