Skip to content

Commit 30f0f89

Browse files
authored
Retrieve configuration from key vault instead of API (#593)
1 parent 22fd2ee commit 30f0f89

File tree

2 files changed

+651
-357
lines changed

2 files changed

+651
-357
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package integration
5+
6+
import (
7+
"context"
8+
"crypto"
9+
"crypto/x509"
10+
"encoding/json"
11+
"fmt"
12+
"net/http"
13+
"os"
14+
"sync"
15+
16+
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
17+
)
18+
19+
// Key Vault URLs
20+
const (
21+
msidLabVault = "https://msidlabs.vault.azure.net"
22+
msalTeamVault = "https://id4skeyvault.vault.azure.net"
23+
)
24+
25+
// Authentication constants
26+
const (
27+
microsoftAuthorityHost = "https://login.microsoftonline.com/"
28+
microsoftAuthority = microsoftAuthorityHost + "microsoft.onmicrosoft.com"
29+
organizationsAuthority = microsoftAuthorityHost + "organizations/"
30+
31+
msIDlabDefaultScope = "https://request.msidlab.com/.default"
32+
graphDefaultScope = "https://graph.windows.net/.default"
33+
34+
defaultClientId = "f62c5ae3-bf3a-4af5-afa8-a68b800396e9"
35+
36+
pemFile = "../../../cert.pem"
37+
ccaPemFile = "../../../ccaCert.pem"
38+
)
39+
40+
// Key Vault secret names - user configs
41+
const (
42+
UserPublicCloud = "User-PublicCloud-Config"
43+
UserFedDefault = "User-Federated-Config"
44+
UserB2C = "MSAL-USER-B2C-JSON"
45+
UserArlington = "MSAL-USER-Arlington-JSON"
46+
UserCIAM = "MSAL-USER-CIAM-JSON"
47+
)
48+
49+
// Key Vault secret names - app configs
50+
const (
51+
AppPCAClient = "App-PCAClient-Config"
52+
AppWebAPI = "App-WebAPI-Config"
53+
AppS2S = "App-S2S-Config"
54+
)
55+
56+
// UserConfig represents user configuration from Key Vault
57+
type UserConfig struct {
58+
AppID string `json:"appId,omitempty"`
59+
ObjectID string `json:"objectId,omitempty"`
60+
UserType string `json:"userType,omitempty"`
61+
DisplayName string `json:"displayName,omitempty"`
62+
Licenses string `json:"licenses,omitempty"`
63+
Upn string `json:"upn,omitempty"`
64+
MFA string `json:"mfa,omitempty"`
65+
ProtectionPolicy string `json:"protectionPolicy,omitempty"`
66+
HomeDomain string `json:"homeDomain,omitempty"`
67+
HomeUPN string `json:"homeUPN,omitempty"`
68+
B2CProvider string `json:"b2cProvider,omitempty"`
69+
LabName string `json:"labName,omitempty"`
70+
LastUpdatedBy string `json:"lastUpdatedBy,omitempty"`
71+
LastUpdatedDate string `json:"lastUpdatedDate,omitempty"`
72+
TenantID string `json:"tenantId,omitempty"`
73+
FederationProvider string `json:"federationProvider,omitempty"`
74+
password string // cached password, fetched lazily
75+
}
76+
77+
// AppConfig represents app configuration from Key Vault
78+
type AppConfig struct {
79+
AppType string `json:"appType,omitempty"`
80+
AppName string `json:"appName,omitempty"`
81+
AppID string `json:"appId,omitempty"`
82+
RedirectURI string `json:"redirectUri,omitempty"`
83+
Authority string `json:"authority,omitempty"`
84+
LabName string `json:"labName,omitempty"`
85+
ClientSecret string `json:"clientSecret,omitempty"`
86+
SecretName string `json:"secretName,omitempty"`
87+
}
88+
89+
// labResponse is the container for JSON from Key Vault
90+
type labResponse struct {
91+
User *UserConfig `json:"user,omitempty"`
92+
App *AppConfig `json:"app,omitempty"`
93+
}
94+
95+
// Package-level cache and Key Vault clients
96+
var (
97+
userCache = make(map[string]*UserConfig)
98+
appCache = make(map[string]*AppConfig)
99+
cacheMu sync.RWMutex
100+
101+
msidClient confidential.Client
102+
msalClient confidential.Client
103+
clientInitMu sync.Once
104+
105+
httpClient = http.Client{}
106+
)
107+
108+
type labClient struct {
109+
app confidential.Client
110+
}
111+
112+
func newLabClient() (*labClient, error) {
113+
cert, privateKey, err := getCertDataFromFile(pemFile)
114+
if err != nil {
115+
return nil, fmt.Errorf("could not get cert data: %w", err)
116+
}
117+
118+
cred, err := confidential.NewCredFromCert(cert, privateKey)
119+
if err != nil {
120+
return nil, fmt.Errorf("could not create a cred from the cert: %w", err)
121+
}
122+
123+
app, err := confidential.New(microsoftAuthority, defaultClientId, cred, confidential.WithX5C())
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
return &labClient{app: app}, nil
129+
}
130+
131+
func (l *labClient) labAccessToken() (string, error) {
132+
scopes := []string{msIDlabDefaultScope}
133+
result, err := l.app.AcquireTokenSilent(context.Background(), scopes)
134+
if err != nil {
135+
result, err = l.app.AcquireTokenByCredential(context.Background(), scopes)
136+
if err != nil {
137+
return "", fmt.Errorf("AcquireTokenByCredential() error: %w", err)
138+
}
139+
}
140+
return result.AccessToken, nil
141+
}
142+
143+
// initKeyVaultClients initializes the Key Vault access clients using cert auth
144+
func initKeyVaultClients() error {
145+
var initErr error
146+
clientInitMu.Do(func() {
147+
cert, privateKey, err := getCertDataFromFile(pemFile)
148+
if err != nil {
149+
initErr = fmt.Errorf("failed to load cert: %w", err)
150+
return
151+
}
152+
153+
cred, err := confidential.NewCredFromCert(cert, privateKey)
154+
if err != nil {
155+
initErr = fmt.Errorf("failed to create cert credential: %w", err)
156+
return
157+
}
158+
159+
// Client for MSID Lab vault (passwords)
160+
msidClient, err = confidential.New(
161+
microsoftAuthority,
162+
defaultClientId,
163+
cred,
164+
confidential.WithX5C(),
165+
)
166+
if err != nil {
167+
initErr = fmt.Errorf("failed to create MSID client: %w", err)
168+
return
169+
}
170+
171+
// Client for MSAL Team vault (configs) - same client works for both
172+
msalClient = msidClient
173+
})
174+
return initErr
175+
}
176+
177+
// GetSecret retrieves a secret from Key Vault by name
178+
func GetSecret(ctx context.Context, vaultURL, secretName string) (string, error) {
179+
if err := initKeyVaultClients(); err != nil {
180+
return "", err
181+
}
182+
183+
scope := vaultURL + "/.default"
184+
result, err := msalClient.AcquireTokenSilent(ctx, []string{scope})
185+
if err != nil {
186+
result, err = msalClient.AcquireTokenByCredential(ctx, []string{scope})
187+
if err != nil {
188+
return "", fmt.Errorf("failed to acquire Key Vault token: %w", err)
189+
}
190+
}
191+
192+
// Use Azure SDK or direct REST call to get secret
193+
// For simplicity, showing the pattern - you'll need to add the actual Key Vault SDK call
194+
secretURL := fmt.Sprintf("%s/secrets/%s?api-version=7.4", vaultURL, secretName)
195+
196+
req, err := http.NewRequestWithContext(ctx, "GET", secretURL, nil)
197+
if err != nil {
198+
return "", err
199+
}
200+
req.Header.Set("Authorization", "Bearer "+result.AccessToken)
201+
202+
resp, err := httpClient.Do(req)
203+
if err != nil {
204+
return "", fmt.Errorf("key Vault request failed: %w", err)
205+
}
206+
defer resp.Body.Close()
207+
208+
if resp.StatusCode != 200 {
209+
return "", fmt.Errorf("key Vault returned status %d", resp.StatusCode)
210+
}
211+
212+
var kvResp struct {
213+
Value string `json:"value"`
214+
}
215+
if err := json.NewDecoder(resp.Body).Decode(&kvResp); err != nil {
216+
return "", err
217+
}
218+
219+
return kvResp.Value, nil
220+
}
221+
222+
// GetUserConfig retrieves and caches user configuration from Key Vault
223+
func GetUserConfig(secretName string) (*UserConfig, error) {
224+
cacheMu.RLock()
225+
if user, ok := userCache[secretName]; ok {
226+
cacheMu.RUnlock()
227+
return user, nil
228+
}
229+
cacheMu.RUnlock()
230+
231+
ctx := context.Background()
232+
secretValue, err := GetSecret(ctx, msalTeamVault, secretName)
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to get user config %s: %w", secretName, err)
235+
}
236+
237+
var resp labResponse
238+
if err := json.Unmarshal([]byte(secretValue), &resp); err != nil {
239+
return nil, fmt.Errorf("failed to parse user config: %w", err)
240+
}
241+
242+
if resp.User == nil {
243+
return nil, fmt.Errorf("no user data in secret %s", secretName)
244+
}
245+
246+
cacheMu.Lock()
247+
userCache[secretName] = resp.User
248+
cacheMu.Unlock()
249+
250+
return resp.User, nil
251+
}
252+
253+
// GetAppConfig retrieves and caches app configuration from Key Vault
254+
func GetAppConfig(secretName string) (*AppConfig, error) {
255+
cacheMu.RLock()
256+
if app, ok := appCache[secretName]; ok {
257+
cacheMu.RUnlock()
258+
return app, nil
259+
}
260+
cacheMu.RUnlock()
261+
262+
ctx := context.Background()
263+
secretValue, err := GetSecret(ctx, msalTeamVault, secretName)
264+
if err != nil {
265+
return nil, fmt.Errorf("failed to get app config %s: %w", secretName, err)
266+
}
267+
268+
var resp labResponse
269+
if err := json.Unmarshal([]byte(secretValue), &resp); err != nil {
270+
return nil, fmt.Errorf("failed to parse app config: %w", err)
271+
}
272+
273+
if resp.App == nil {
274+
return nil, fmt.Errorf("no app data in secret %s", secretName)
275+
}
276+
277+
cacheMu.Lock()
278+
appCache[secretName] = resp.App
279+
cacheMu.Unlock()
280+
281+
return resp.App, nil
282+
}
283+
284+
// GetPassword retrieves the user's password from MSID Lab Key Vault
285+
// This is a method on UserConfig for convenience, fetched lazily
286+
func (u *UserConfig) GetPassword() (string, error) {
287+
if u.password != "" {
288+
return u.password, nil
289+
}
290+
291+
if u.LabName == "" {
292+
return "", fmt.Errorf("user has no lab name for password lookup")
293+
}
294+
295+
ctx := context.Background()
296+
password, err := GetSecret(ctx, msidLabVault, u.LabName)
297+
if err != nil {
298+
return "", fmt.Errorf("failed to get password for %s: %w", u.LabName, err)
299+
}
300+
301+
u.password = password
302+
return password, nil
303+
}
304+
305+
// getCertDataFromFile loads certificate and private key from a PEM file
306+
func getCertDataFromFile(filePath string) ([]*x509.Certificate, crypto.PrivateKey, error) {
307+
data, err := os.ReadFile(filePath)
308+
if err != nil {
309+
return nil, nil, fmt.Errorf("error reading certificate file: %w", err)
310+
}
311+
312+
cert, privateKey, err := confidential.CertFromPEM(data, "")
313+
if err != nil {
314+
return nil, nil, fmt.Errorf("error parsing certificate: %w", err)
315+
}
316+
317+
return cert, privateKey, nil
318+
}

0 commit comments

Comments
 (0)