Skip to content

Commit 6745e7b

Browse files
ishikap-metronvishalk-metronkapil-metron
authored
feat: Managedidentity sdk implementation (#150)
* test commit * Revert "test commit" This reverts commit a50fd8b. * Initial commit with code update of Managed Idenity SDK Implementation * Addressed the respective PR comments. --------- Co-authored-by: vishalk-metron <vishal.khemnar@metronlabs.com> Co-authored-by: kapil-metron <58544320+kapil-metron@users.noreply.github.com>
1 parent 3beec51 commit 6745e7b

8 files changed

Lines changed: 213 additions & 76 deletions

File tree

client/config/config.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,25 @@ import (
2424
)
2525

2626
type Config struct {
27-
ApplicationId string // The Application Id that the Azure app registration portal assigned when the app was registered.
28-
Authority string // The Azure ActiveDirectory Authority URL
29-
ClientSecret string // The Application Secret that was generated for the app in the app registration portal.
30-
ClientCert string // The certificate uploaded to the app registration portal."
31-
ClientKey string // The key for a certificate uploaded to the app registration portal."
32-
ClientKeyPass string // The passphrase to use in conjuction with the associated key of a certificate uploaded to the app registration portal."
33-
Graph string // The Microsoft Graph URL
34-
JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs
35-
Management string // The Azure ResourceManager URL
36-
MgmtGroupId []string // The Management Group Id to use as a filter
37-
ManagedIdentity bool // If true then the client will use a managed identity to authenticate to Azure APIs
38-
Password string // The password associated with the user principal name associated with the Azure portal.
39-
ProxyUrl string // The forward proxy url
40-
RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs
41-
Region string // The region of the Azure Cloud deployment.
42-
SubscriptionId []string // The Subscription Id(s) to use as a filter
43-
Tenant string // The directory tenant that you want to request permission from. This can be in GUID or friendly name format
44-
Username string // The user principal name associated with the Azure portal.
27+
ApplicationId string // The Application Id that the Azure app registration portal assigned when the app was registered.
28+
Authority string // The Azure ActiveDirectory Authority URL
29+
ClientSecret string // The Application Secret that was generated for the app in the app registration portal.
30+
ClientCert string // The certificate uploaded to the app registration portal."
31+
ClientKey string // The key for a certificate uploaded to the app registration portal."
32+
ClientKeyPass string // The passphrase to use in conjuction with the associated key of a certificate uploaded to the app registration portal."
33+
Graph string // The Microsoft Graph URL
34+
JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs
35+
Management string // The Azure ResourceManager URL
36+
MgmtGroupId []string // The Management Group Id to use as a filter
37+
ManagedIdentity bool // If true then the client will use a managed identity to authenticate to Azure APIs
38+
ManagedIdentityClientId string // Client ID of user-assigned managed idenity used to authenticate via managed identity SDK
39+
Password string // The password associated with the user principal name associated with the Azure portal.
40+
ProxyUrl string // The forward proxy url
41+
RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs
42+
Region string // The region of the Azure Cloud deployment.
43+
SubscriptionId []string // The Subscription Id(s) to use as a filter
44+
Tenant string // The directory tenant that you want to request permission from. This can be in GUID or friendly name format
45+
Username string // The user principal name associated with the Azure portal.
4546
}
4647

4748
func AuthorityUrl(region string, defaultUrl string) string {

client/rest/auth.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import (
2424
"net/http"
2525
"net/url"
2626
"sync"
27+
"time"
2728

29+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
30+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
31+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
2832
"github.com/bloodhoundad/azurehound/v2/client/config"
2933
"github.com/bloodhoundad/azurehound/v2/constants"
3034
)
@@ -52,6 +56,14 @@ type ManagedIdentityAuthStrategy struct {
5256
token Token
5357
}
5458

59+
// ManagedIdentitySDKAuthStrategy is an authentication strategy that uses Azure Managed Identity SDK
60+
type ManagedIdentitySDKAuthStrategy struct {
61+
credential *azidentity.ManagedIdentityCredential
62+
resource url.URL
63+
token azcore.AccessToken
64+
mutex sync.Mutex
65+
}
66+
5567
// GenericAuthStrategy is an authentication strategy that uses a bunch of pre-existing authentication methods (TODO: Break this up)
5668
type GenericAuthStrategy struct {
5769
config config.Config
@@ -83,6 +95,32 @@ func NewManagedIdentityAuthenticator(config config.Config, auth *url.URL, api *u
8395
}
8496
}
8597

98+
func GetManagedIdentityCredential(config config.Config) (*azidentity.ManagedIdentityCredential, error) {
99+
options := &azidentity.ManagedIdentityCredentialOptions{
100+
ID: azidentity.ClientID(config.ManagedIdentityClientId),
101+
}
102+
cred, err := azidentity.NewManagedIdentityCredential(options)
103+
if err != nil {
104+
optionsFallback := &azidentity.ManagedIdentityCredentialOptions{}
105+
cred, err = azidentity.NewManagedIdentityCredential(optionsFallback)
106+
if err != nil {
107+
return nil, fmt.Errorf("failed to authenticate with managed identity: %w", err)
108+
}
109+
}
110+
return cred, nil
111+
}
112+
113+
// NewManagedIdentitySDKAuthenticator creates a new Authenticator using the ManagedIdentitySDKAuthStrategy
114+
func NewManagedIdentitySDKAuthenticator(config config.Config, resource *url.URL, cred *azidentity.ManagedIdentityCredential) *Authenticator {
115+
return &Authenticator{
116+
auth: &ManagedIdentitySDKAuthStrategy{
117+
credential: cred,
118+
resource: *resource,
119+
},
120+
mutex: sync.RWMutex{},
121+
}
122+
}
123+
86124
// NewGenericAuthenticator creates a new Authenticator using the GenericAuthStrategy (The collection of pre-existing authentication methods)
87125
func NewGenericAuthenticator(config config.Config, auth *url.URL, api *url.URL) *Authenticator {
88126
return &Authenticator{
@@ -123,9 +161,14 @@ func (s *Authenticator) refreshIfExpired(r *restClient) error {
123161
return nil
124162
}
125163
// Authenticate
126-
if authRequest, err := s.auth.createAuthRequest(); err != nil {
164+
authRequest, err := s.auth.createAuthRequest()
165+
if err != nil {
127166
return err
128-
} else if authResponse, err := r.send(authRequest); err != nil {
167+
}
168+
if authRequest == nil {
169+
return nil
170+
}
171+
if authResponse, err := r.send(authRequest); err != nil {
129172
return err
130173
} else {
131174
defer authResponse.Body.Close()
@@ -249,3 +292,43 @@ func (s *GenericAuthStrategy) addAuthenticationToRequest(req *http.Request) (*ht
249292
}
250293
return req, nil
251294
}
295+
296+
// Adds the access token to the outgoing HTTP request
297+
func (s *ManagedIdentitySDKAuthStrategy) addAuthenticationToRequest(req *http.Request) (*http.Request, error) {
298+
token := s.token
299+
s.mutex.Lock()
300+
defer s.mutex.Unlock()
301+
if token.Token == "" || token.ExpiresOn.Before(time.Now().Add(2*time.Minute)) {
302+
if err := s.refreshToken(req.Context()); err != nil {
303+
return nil, err
304+
}
305+
token = s.token
306+
}
307+
req.Header.Set("Authorization", "Bearer "+token.Token)
308+
return req, nil
309+
}
310+
311+
func (s *ManagedIdentitySDKAuthStrategy) refreshToken(ctx context.Context) error {
312+
token, err := s.credential.GetToken(ctx, policy.TokenRequestOptions{
313+
Scopes: []string{s.resource.String() + "/.default"},
314+
})
315+
if err != nil {
316+
return err
317+
}
318+
s.token = token
319+
return nil
320+
}
321+
322+
func (s *ManagedIdentitySDKAuthStrategy) createAuthRequest() (*http.Request, error) {
323+
// Not used in SDK-based flow, return nil
324+
return nil, nil
325+
}
326+
327+
func (s *ManagedIdentitySDKAuthStrategy) decodeAuthResponse(resp *http.Response) error {
328+
// Not used in SDK-based flow, do nothing
329+
return nil
330+
}
331+
332+
func (s *ManagedIdentitySDKAuthStrategy) isExpired() bool {
333+
return s.token.Token == "" || s.token.ExpiresOn.Before(time.Now().Add(2*time.Minute))
334+
}

client/rest/client.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ func NewRestClient(apiUrl string, config config.Config) (RestClient, error) {
5555
} else {
5656
var authenticator *Authenticator
5757
if config.ManagedIdentity {
58-
authenticator = NewManagedIdentityAuthenticator(config, auth, api, http)
58+
cred, err := GetManagedIdentityCredential(config)
59+
if err != nil {
60+
return nil, err
61+
}
62+
authenticator = NewManagedIdentitySDKAuthenticator(config, api, cred)
63+
5964
} else {
6065
authenticator = NewGenericAuthenticator(config, auth, api)
6166
}
@@ -73,12 +78,12 @@ func NewRestClient(apiUrl string, config config.Config) (RestClient, error) {
7378
}
7479

7580
type restClient struct {
76-
api url.URL
77-
http *http.Client
78-
tenant string
79-
token Token
80-
subId []string
81-
mgmtGroupId []string
81+
api url.URL
82+
http *http.Client
83+
tenant string
84+
token Token
85+
subId []string
86+
mgmtGroupId []string
8287
Authenticator *Authenticator
8388
}
8489

cmd/configure.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ var configureCmd = &cobra.Command{
5656
SilenceUsage: true,
5757
}
5858

59+
var ManagedIdentityTypes = []string{
60+
"System-Assigned",
61+
"User-Assigned",
62+
}
63+
5964
func configureCmdImpl(cmd *cobra.Command, args []string) {
6065
if err := configure(); err != nil {
6166
exit(fmt.Errorf("failed to configure cobra CLI: %w", err))
@@ -116,6 +121,19 @@ func configure() error {
116121
}
117122
} else if authMethod == enums.ManagedIdentity {
118123
config.AzUseManagedIdentity.Set(true)
124+
if _, identityType, err := choose("Managed Identity Type", ManagedIdentityTypes, 0); err != nil {
125+
return err
126+
} else if identityType == "User-Assigned" {
127+
// User-Assigned: Prompt for Client ID
128+
if umiClient, err := prompt("Input the User-Assigned Managed Identity (Client ID)", validateGuid, true); err != nil {
129+
return err
130+
} else {
131+
config.AzManagedIdentityClientId.Set(umiClient)
132+
}
133+
} else {
134+
// System-Assigned: Set client ID to empty string
135+
config.AzManagedIdentityClientId.Set("")
136+
}
119137
} else if secret, err := prompt("Client Secret", nil, true); err != nil {
120138
return err
121139
} else {

cmd/utils.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,24 +125,25 @@ func newAzureClient() (client.AzureClient, error) {
125125
}
126126

127127
config := client_config.Config{
128-
ApplicationId: config.AzAppId.Value().(string),
129-
Authority: config.AzAuthUrl.Value().(string),
130-
ClientSecret: config.AzSecret.Value().(string),
131-
ClientCert: clientCert,
132-
ClientKey: clientKey,
133-
ClientKeyPass: config.AzKeyPass.Value().(string),
134-
Graph: config.AzGraphUrl.Value().(string),
135-
JWT: config.JWT.Value().(string),
136-
Management: config.AzMgmtUrl.Value().(string),
137-
MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
138-
Password: config.AzPassword.Value().(string),
139-
ProxyUrl: config.Proxy.Value().(string),
140-
RefreshToken: config.RefreshToken.Value().(string),
141-
Region: config.AzRegion.Value().(string),
142-
SubscriptionId: config.AzSubId.Value().([]string),
143-
Tenant: config.AzTenant.Value().(string),
144-
Username: config.AzUsername.Value().(string),
145-
ManagedIdentity: config.AzUseManagedIdentity.Value().(bool),
128+
ApplicationId: config.AzAppId.Value().(string),
129+
Authority: config.AzAuthUrl.Value().(string),
130+
ClientSecret: config.AzSecret.Value().(string),
131+
ClientCert: clientCert,
132+
ClientKey: clientKey,
133+
ClientKeyPass: config.AzKeyPass.Value().(string),
134+
Graph: config.AzGraphUrl.Value().(string),
135+
JWT: config.JWT.Value().(string),
136+
Management: config.AzMgmtUrl.Value().(string),
137+
MgmtGroupId: config.AzMgmtGroupId.Value().([]string),
138+
Password: config.AzPassword.Value().(string),
139+
ProxyUrl: config.Proxy.Value().(string),
140+
RefreshToken: config.RefreshToken.Value().(string),
141+
Region: config.AzRegion.Value().(string),
142+
SubscriptionId: config.AzSubId.Value().([]string),
143+
Tenant: config.AzTenant.Value().(string),
144+
Username: config.AzUsername.Value().(string),
145+
ManagedIdentity: config.AzUseManagedIdentity.Value().(bool),
146+
ManagedIdentityClientId: config.AzManagedIdentityClientId.Value().(string),
146147
}
147148
return client.NewClient(config)
148149
}

config/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ var AzRegions = []string{
7777
constants.USGovL5,
7878
}
7979

80+
var ManagedIdentityTypes = []string{
81+
"System-Assigned Identity",
82+
"User-Assigned Identity",
83+
}
84+
8085
var (
8186
// Global Configurations
8287
ConfigFile = Config{
@@ -244,6 +249,13 @@ var (
244249
Default: bool(false),
245250
}
246251

252+
AzManagedIdentityClientId = Config{
253+
Name: "managed-identity-client-id",
254+
Shorthand: "",
255+
Usage: "Client ID used to authenticate via Managed Identity SDK",
256+
Persistent: true,
257+
Default: "",
258+
}
247259
// BHE Configurations
248260
BHEUrl = Config{
249261
Name: "instance",
@@ -370,6 +382,7 @@ var (
370382
AzSubId,
371383
AzMgmtGroupId,
372384
AzUseManagedIdentity,
385+
AzManagedIdentityClientId,
373386
}
374387

375388
BloodHoundEnterpriseConfig = []Config{

go.mod

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ go 1.23.0
55
toolchain go1.24.2
66

77
require (
8+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2
9+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0
810
github.com/go-logr/logr v1.4.3
911
github.com/gofrs/uuid v4.4.0+incompatible
10-
github.com/golang-jwt/jwt/v5 v5.2.2
12+
github.com/golang-jwt/jwt/v5 v5.3.0
1113
github.com/judwhite/go-svc v1.2.1
1214
github.com/manifoldco/promptui v0.9.0
1315
github.com/rs/zerolog v1.34.0
@@ -17,31 +19,32 @@ require (
1719
github.com/stretchr/testify v1.10.0
1820
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78
1921
go.uber.org/mock v0.5.2
20-
golang.org/x/net v0.40.0
21-
golang.org/x/sys v0.33.0
22+
golang.org/x/net v0.42.0
23+
golang.org/x/sys v0.34.0
2224
)
2325

2426
require (
27+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
28+
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
2529
github.com/chzyer/readline v1.5.1 // indirect
2630
github.com/davecgh/go-spew v1.1.1 // indirect
2731
github.com/fsnotify/fsnotify v1.9.0 // indirect
2832
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
33+
github.com/google/uuid v1.6.0 // indirect
2934
github.com/inconshreveable/mousetrap v1.1.0 // indirect
35+
github.com/kylelemons/godebug v1.1.0 // indirect
3036
github.com/mattn/go-colorable v0.1.14 // indirect
3137
github.com/mattn/go-isatty v0.0.20 // indirect
3238
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
39+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3340
github.com/pmezard/go-difflib v1.0.0 // indirect
3441
github.com/sagikazarmark/locafero v0.9.0 // indirect
3542
github.com/sourcegraph/conc v0.3.0 // indirect
3643
github.com/spf13/afero v1.14.0 // indirect
3744
github.com/spf13/cast v1.8.0 // indirect
3845
github.com/subosito/gotenv v1.6.0 // indirect
39-
github.com/yuin/goldmark v1.4.13 // indirect
4046
go.uber.org/multierr v1.11.0 // indirect
41-
golang.org/x/crypto v0.38.0 // indirect
42-
golang.org/x/mod v0.18.0 // indirect
43-
golang.org/x/sync v0.14.0 // indirect
44-
golang.org/x/text v0.25.0 // indirect
45-
golang.org/x/tools v0.22.0 // indirect
47+
golang.org/x/crypto v0.40.0 // indirect
48+
golang.org/x/text v0.27.0 // indirect
4649
gopkg.in/yaml.v3 v3.0.1 // indirect
4750
)

0 commit comments

Comments
 (0)