Skip to content

Commit 68d73d5

Browse files
daviditkinddlees
andauthored
Feature managed identity auth (#94)
* Authentictor manages authentication process using a specific AuthStrategy implementations. ManagedIdentityAuthStrategy now supported. * refactor restClient to delegate to Authenticator to authenticate. * handle auth responses that have int or string of digits for expiration times. Added IntOrStringInt. * test parsing of different types of auth responses. * Added managed-identity configuration to support managed identity authentication strategy. * configure supports managed-identity true in config.json. * minor comment --------- Co-authored-by: Dillon Lees <[email protected]>
1 parent c89c9a3 commit 68d73d5

File tree

12 files changed

+412
-131
lines changed

12 files changed

+412
-131
lines changed

client/config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Config struct {
3434
JWT string // The JSON web token that will be used to authenticate requests sent to Azure APIs
3535
Management string // The Azure ResourceManager URL
3636
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
3738
Password string // The password associated with the user principal name associated with the Azure portal.
3839
ProxyUrl string // The forward proxy url
3940
RefreshToken string // The refresh token that will be used to authenticate requests sent to Azure APIs

client/rest/auth.go

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright (C) 2024 Specter Ops, Inc.
2+
//
3+
// This file is part of AzureHound.
4+
//
5+
// AzureHound is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// AzureHound is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
package rest
19+
20+
import (
21+
"context"
22+
"encoding/json"
23+
"fmt"
24+
"net/http"
25+
"net/url"
26+
"sync"
27+
28+
"github.com/bloodhoundad/azurehound/v2/client/config"
29+
"github.com/bloodhoundad/azurehound/v2/constants"
30+
)
31+
32+
// AuthStrategy is an interface that defines the methods that an authentication strategy must implement
33+
type AuthStrategy interface {
34+
isExpired() bool
35+
createAuthRequest() (*http.Request, error)
36+
decodeAuthResponse(resp *http.Response) error
37+
addAuthenticationToRequest(req *http.Request) (*http.Request, error)
38+
}
39+
40+
// Authenticator manages the authentication process, using a specific AuthStrategy
41+
type Authenticator struct {
42+
auth AuthStrategy
43+
mutex sync.RWMutex
44+
}
45+
46+
// ManagedIdentityAuthStrategy is an authentication strategy that uses Azure Managed Identity
47+
type ManagedIdentityAuthStrategy struct {
48+
config config.Config
49+
authUrl url.URL
50+
api url.URL
51+
tenant string
52+
token Token
53+
}
54+
55+
// GenericAuthStrategy is an authentication strategy that uses a bunch of pre-existing authentication methods (TODO: Break this up)
56+
type GenericAuthStrategy struct {
57+
config config.Config
58+
api url.URL
59+
authUrl url.URL
60+
jwt string
61+
clientId string
62+
clientSecret string
63+
clientCert string
64+
clientKey string
65+
clientKeyPass string
66+
username string
67+
password string
68+
refreshToken string
69+
tenant string
70+
token Token
71+
}
72+
73+
// NewManagedIdentityAuthenticator creates a new Authenticator using the ManagedIdentityAuthStrategy
74+
func NewManagedIdentityAuthenticator(config config.Config, auth *url.URL, api *url.URL, http *http.Client) *Authenticator {
75+
return &Authenticator{
76+
auth: &ManagedIdentityAuthStrategy{
77+
config: config,
78+
authUrl: *auth,
79+
api: *api,
80+
tenant: config.Tenant,
81+
},
82+
mutex: sync.RWMutex{},
83+
}
84+
}
85+
86+
// NewGenericAuthenticator creates a new Authenticator using the GenericAuthStrategy (The collection of pre-existing authentication methods)
87+
func NewGenericAuthenticator(config config.Config, auth *url.URL, api *url.URL) *Authenticator {
88+
return &Authenticator{
89+
auth: &GenericAuthStrategy{config: config,
90+
authUrl: *auth,
91+
api: *api,
92+
jwt: config.JWT,
93+
clientId: config.ApplicationId,
94+
clientSecret: config.ClientSecret,
95+
clientCert: config.ClientCert,
96+
clientKey: config.ClientKey,
97+
clientKeyPass: config.ClientKeyPass,
98+
username: config.Username,
99+
password: config.Password,
100+
refreshToken: config.RefreshToken,
101+
tenant: config.Tenant,
102+
token: Token{},
103+
},
104+
mutex: sync.RWMutex{},
105+
}
106+
}
107+
108+
// Authenticate if needed and add authentication to the request
109+
func (s *Authenticator) AddAuthenticationToRequest(restClient *restClient, req *http.Request) (*http.Request, error) {
110+
if err := s.refreshIfExpired(restClient); err != nil {
111+
return nil, err
112+
}
113+
if req, err := s.auth.addAuthenticationToRequest(req); err != nil {
114+
return nil, err
115+
} else {
116+
return req, err
117+
}
118+
}
119+
120+
// Authenticate if needed using a specific AuthStrategy
121+
func (s *Authenticator) refreshIfExpired(r *restClient) error {
122+
if !s.auth.isExpired() {
123+
return nil
124+
}
125+
// Authenticate
126+
if authRequest, err := s.auth.createAuthRequest(); err != nil {
127+
return err
128+
} else if authResponse, err := r.send(authRequest); err != nil {
129+
return err
130+
} else {
131+
defer authResponse.Body.Close()
132+
s.mutex.Lock()
133+
defer s.mutex.Unlock()
134+
135+
if err := s.auth.decodeAuthResponse(authResponse); err != nil {
136+
return err
137+
}
138+
}
139+
return nil
140+
}
141+
142+
func (s *ManagedIdentityAuthStrategy) isExpired() bool {
143+
return s.token.IsExpired()
144+
}
145+
146+
func (s *ManagedIdentityAuthStrategy) addAuthenticationToRequest(req *http.Request) (*http.Request, error) {
147+
req.Header.Set("Authorization", s.token.String())
148+
149+
return req, nil
150+
}
151+
152+
func (s *ManagedIdentityAuthStrategy) createAuthRequest() (*http.Request, error) {
153+
endpoint, err := url.Parse("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01")
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
getArgs := endpoint.Query()
159+
getArgs.Add("resource", s.api.String())
160+
endpoint.RawQuery = getArgs.Encode()
161+
162+
req, err := NewRequest(context.Background(), "GET", endpoint, nil, nil, nil)
163+
if err != nil {
164+
return nil, err
165+
}
166+
req.Header.Add("Metadata", "true")
167+
168+
return req, nil
169+
}
170+
171+
func (s *ManagedIdentityAuthStrategy) decodeAuthResponse(resp *http.Response) error {
172+
if err := json.NewDecoder(resp.Body).Decode(&s.token); err != nil {
173+
return err
174+
} else {
175+
return nil
176+
}
177+
}
178+
179+
func (s *GenericAuthStrategy) createAuthRequest() (*http.Request, error) {
180+
var (
181+
path = url.URL{Path: fmt.Sprintf("/%s/oauth2/v2.0/token", s.tenant)}
182+
endpoint = s.authUrl.ResolveReference(&path)
183+
defaultScope = url.URL{Path: "/.default"}
184+
scope = s.api.ResolveReference(&defaultScope)
185+
body = url.Values{}
186+
)
187+
188+
if s.clientId == "" {
189+
body.Add("client_id", constants.AzPowerShellClientID)
190+
} else {
191+
body.Add("client_id", s.clientId)
192+
}
193+
194+
body.Add("scope", scope.ResolveReference(&defaultScope).String())
195+
196+
if s.refreshToken != "" {
197+
body.Add("grant_type", "refresh_token")
198+
body.Add("refresh_token", s.refreshToken)
199+
body.Set("client_id", constants.AzPowerShellClientID)
200+
} else if s.clientSecret != "" {
201+
body.Add("grant_type", "client_credentials")
202+
body.Add("client_secret", s.clientSecret)
203+
} else if s.clientCert != "" && s.clientKey != "" {
204+
if clientAssertion, err := NewClientAssertion(endpoint.String(), s.clientId, s.clientCert, s.clientKey, s.clientKeyPass); err != nil {
205+
return nil, err
206+
} else {
207+
body.Add("grant_type", "client_credentials")
208+
body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
209+
body.Add("client_assertion", clientAssertion)
210+
}
211+
} else if s.username != "" && s.password != "" {
212+
body.Add("grant_type", "password")
213+
body.Add("username", s.username)
214+
body.Add("password", s.password)
215+
body.Set("client_id", constants.AzPowerShellClientID)
216+
} else {
217+
return nil, fmt.Errorf("unable to authenticate. no valid credential provided")
218+
}
219+
220+
if authRequest, err := NewRequest(context.Background(), "POST", endpoint, body, nil, nil); err != nil {
221+
return nil, err
222+
} else {
223+
return authRequest, nil
224+
}
225+
}
226+
227+
func (s *GenericAuthStrategy) isExpired() bool {
228+
return s.token.IsExpired()
229+
}
230+
231+
func (s *GenericAuthStrategy) decodeAuthResponse(resp *http.Response) error {
232+
if err := json.NewDecoder(resp.Body).Decode(&s.token); err != nil {
233+
return err
234+
} else {
235+
return nil
236+
}
237+
}
238+
239+
func (s *GenericAuthStrategy) addAuthenticationToRequest(req *http.Request) (*http.Request, error) {
240+
if s.jwt != "" {
241+
if aud, err := ParseAud(s.jwt); err != nil {
242+
return nil, err
243+
} else if aud != s.api.String() {
244+
return nil, fmt.Errorf("invalid audience")
245+
}
246+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.jwt))
247+
} else {
248+
req.Header.Set("Authorization", s.token.String())
249+
}
250+
return req, nil
251+
}

0 commit comments

Comments
 (0)