@@ -9,13 +9,19 @@ import (
99 "context"
1010 "encoding/base64"
1111 "fmt"
12+ "path/filepath"
1213 "regexp"
1314 "strings"
1415 "time"
1516
17+ "encoding/json"
18+ "os"
19+
1620 "github.com/Azure/azure-sdk-for-go/sdk/azcore"
21+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1722 "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
1823 "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
24+ azidentitycache "github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache"
1925 "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
2026 "github.com/sirupsen/logrus"
2127
@@ -25,6 +31,8 @@ import (
2531const (
2632 // KeyTypeIdentifier is the string used to identify an Azure Key Vault MasterKey.
2733 KeyTypeIdentifier = "azure_kv"
34+
35+ sopsAzureAuthMethod = "SOPS_AZURE_AUTH_METHOD"
2836)
2937
3038var (
@@ -230,7 +238,132 @@ func (key *MasterKey) TypeToIdentifier() string {
230238// azidentity.NewDefaultAzureCredential.
231239func (key * MasterKey ) getTokenCredential () (azcore.TokenCredential , error ) {
232240 if key .tokenCredential == nil {
233- return azidentity .NewDefaultAzureCredential (nil )
241+
242+ authMethod := strings .ToUpper (os .Getenv (sopsAzureAuthMethod ))
243+ switch authMethod {
244+ case "BROWSER" :
245+
246+ return cachedInteractiveBrowserCredentials ()
247+
248+ case "DEVICE_CODE" :
249+ return cachedDeviceCodeCredentials ()
250+ case "AZURE_CLI" :
251+ return azidentity .NewAzureCLICredential (nil )
252+ case "MSI" :
253+ return azidentity .NewManagedIdentityCredential (nil )
254+ // If "DEFAULT" or not explicitly specified then use the default authentication chain.
255+ case "" , "DEFAULT" :
256+ return azidentity .NewDefaultAzureCredential (nil )
257+ default :
258+ return nil , fmt .Errorf ("Value `%s` is unsupported for `%s`, to resolve this either leave it unset or use one of DEFAULT/MSI/BROWSER/DEVICE_CODE" , authMethod , sopsAzureAuthMethod )
259+ }
234260 }
235261 return key .tokenCredential , nil
236262}
263+
264+ func sopsCacheDir () (string , error ) {
265+ userCacheDir , err := os .UserCacheDir ()
266+ if err != nil {
267+ return "" , err
268+ }
269+
270+ cacheDir := filepath .Join (userCacheDir , "/sops" )
271+
272+ if err = os .MkdirAll (cacheDir , 0o700 ); err != nil {
273+ return "" , err
274+ }
275+
276+ return cacheDir , nil
277+ }
278+
279+ type CachableTokenCredential interface {
280+ Authenticate (ctx context.Context , opts * policy.TokenRequestOptions ) (azidentity.AuthenticationRecord , error )
281+ GetToken (ctx context.Context , opts policy.TokenRequestOptions ) (azcore.AccessToken , error )
282+ }
283+
284+ func cacheStoreRecord (cachePath string , record azidentity.AuthenticationRecord ) error {
285+ b , err := json .Marshal (record )
286+ if err != nil {
287+ return err
288+ }
289+
290+ return os .WriteFile (cachePath , b , 0600 )
291+ }
292+
293+ func cacheLoadRecord (cachePath string ) (azidentity.AuthenticationRecord , error ) {
294+ var record azidentity.AuthenticationRecord
295+
296+ b , err := os .ReadFile (cachePath )
297+ if err != nil {
298+ return record , err
299+ }
300+
301+ err = json .Unmarshal (b , & record )
302+ if err != nil {
303+ return record , err
304+ }
305+
306+ return record , nil
307+ }
308+
309+ func cacheTokenCredential (cachePath string , tokenCredentialFn func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error )) (azcore.TokenCredential , error ) {
310+ cache , err := azidentitycache .New (nil )
311+ // Errors if persistent caching is not supported by the current runtime
312+ if err != nil {
313+ return nil , err
314+ }
315+
316+ cachedRecord , cacheLoadErr := cacheLoadRecord (cachePath )
317+
318+ credential , err := tokenCredentialFn (cache , cachedRecord )
319+ if err != nil {
320+ return nil , err
321+ }
322+
323+ // If loading the authenticationRecord from the cachePath failed for any reason (validation, file doesn't exist, not encoded using json, etc.)
324+ if cacheLoadErr != nil {
325+ record , err := credential .Authenticate (context .Background (), nil )
326+ if err != nil {
327+ return nil , err
328+ }
329+
330+ if err = cacheStoreRecord (cachePath , record ); err != nil {
331+ return nil , err
332+ }
333+ }
334+
335+ return credential , nil
336+ }
337+
338+ func cachedInteractiveBrowserCredentials () (azcore.TokenCredential , error ) {
339+ cacheDir , err := sopsCacheDir ()
340+ if err != nil {
341+ return nil , err
342+ }
343+ return cacheTokenCredential (
344+ filepath .Join (cacheDir , "azure_auth_record_browser.json" ),
345+ func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error ) {
346+ return azidentity .NewInteractiveBrowserCredential (& azidentity.InteractiveBrowserCredentialOptions {
347+ AuthenticationRecord : record ,
348+ Cache : cache ,
349+ })
350+ },
351+ )
352+ }
353+
354+ func cachedDeviceCodeCredentials () (azcore.TokenCredential , error ) {
355+ cacheDir , err := sopsCacheDir ()
356+ if err != nil {
357+ return nil , err
358+ }
359+
360+ return cacheTokenCredential (
361+ filepath .Join (cacheDir , "azure_auth_record_device_code.json" ),
362+ func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error ) {
363+ return azidentity .NewDeviceCodeCredential (& azidentity.DeviceCodeCredentialOptions {
364+ AuthenticationRecord : record ,
365+ Cache : cache ,
366+ })
367+ },
368+ )
369+ }
0 commit comments