@@ -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,11 @@ import (
2531const (
2632 // KeyTypeIdentifier is the string used to identify an Azure Key Vault MasterKey.
2733 KeyTypeIdentifier = "azure_kv"
34+
35+ SopsAzureAuthMethodEnv = "SOPS_AZURE_AUTH_METHOD"
36+
37+ cachedBrowserAuthRecordFileName = "azure-auth-record-browser.json"
38+ cachedDeviceCodeAuthRecordFileName = "azure-auth-record-device-code.json"
2839)
2940
3041var (
@@ -230,7 +241,130 @@ func (key *MasterKey) TypeToIdentifier() string {
230241// azidentity.NewDefaultAzureCredential.
231242func (key * MasterKey ) getTokenCredential () (azcore.TokenCredential , error ) {
232243 if key .tokenCredential == nil {
233- return azidentity .NewDefaultAzureCredential (nil )
244+
245+ authMethod := strings .ToLower (os .Getenv (SopsAzureAuthMethodEnv ))
246+ switch authMethod {
247+ case "cached-browser" :
248+ return cachedInteractiveBrowserCredentials ()
249+ case "cached-device-code" :
250+ return cachedDeviceCodeCredentials ()
251+ case "azure-cli" :
252+ return azidentity .NewAzureCLICredential (nil )
253+ case "msi" :
254+ return azidentity .NewManagedIdentityCredential (nil )
255+ // If "DEFAULT" or not explicitly specified then use the default authentication chain.
256+ case "" , "default" :
257+ return azidentity .NewDefaultAzureCredential (nil )
258+ default :
259+ return nil , fmt .Errorf ("Value `%s` is unsupported for environment variable `%s`, to resolve this either leave it unset or use one of `default`/`msi`/`azure-cli`/`cached-browser`/`cached-device-code`" , authMethod , SopsAzureAuthMethodEnv )
260+ }
234261 }
235262 return key .tokenCredential , nil
236263}
264+
265+ func sopsCacheDir () (string , error ) {
266+ userCacheDir , err := os .UserCacheDir ()
267+ if err != nil {
268+ return "" , err
269+ }
270+
271+ cacheDir := filepath .Join (userCacheDir , "/sops" )
272+
273+ if err = os .MkdirAll (cacheDir , 0o700 ); err != nil {
274+ return "" , err
275+ }
276+
277+ return cacheDir , nil
278+ }
279+
280+ type CachableTokenCredential interface {
281+ Authenticate (ctx context.Context , opts * policy.TokenRequestOptions ) (azidentity.AuthenticationRecord , error )
282+ GetToken (ctx context.Context , opts policy.TokenRequestOptions ) (azcore.AccessToken , error )
283+ }
284+
285+ func cacheStoreRecord (cachePath string , record azidentity.AuthenticationRecord ) error {
286+ b , err := json .Marshal (record )
287+ if err != nil {
288+ return err
289+ }
290+
291+ return os .WriteFile (cachePath , b , 0600 )
292+ }
293+
294+ func cacheLoadRecord (cachePath string ) (azidentity.AuthenticationRecord , error ) {
295+ var record azidentity.AuthenticationRecord
296+
297+ b , err := os .ReadFile (cachePath )
298+ if err != nil {
299+ return record , err
300+ }
301+
302+ err = json .Unmarshal (b , & record )
303+ if err != nil {
304+ return record , err
305+ }
306+
307+ return record , nil
308+ }
309+
310+ func cacheTokenCredential (cachePath string , tokenCredentialFn func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error )) (azcore.TokenCredential , error ) {
311+ cache , err := azidentitycache .New (nil )
312+ // Errors if persistent caching is not supported by the current runtime
313+ if err != nil {
314+ return nil , err
315+ }
316+
317+ cachedRecord , cacheLoadErr := cacheLoadRecord (cachePath )
318+
319+ credential , err := tokenCredentialFn (cache , cachedRecord )
320+ if err != nil {
321+ return nil , err
322+ }
323+
324+ // If loading the authenticationRecord from the cachePath failed for any reason (validation, file doesn't exist, not encoded using json, etc.)
325+ if cacheLoadErr != nil {
326+ record , err := credential .Authenticate (context .Background (), nil )
327+ if err != nil {
328+ return nil , err
329+ }
330+
331+ if err = cacheStoreRecord (cachePath , record ); err != nil {
332+ return nil , err
333+ }
334+ }
335+
336+ return credential , nil
337+ }
338+
339+ func cachedInteractiveBrowserCredentials () (azcore.TokenCredential , error ) {
340+ cacheDir , err := sopsCacheDir ()
341+ if err != nil {
342+ return nil , err
343+ }
344+ return cacheTokenCredential (
345+ filepath .Join (cacheDir , cachedBrowserAuthRecordFileName ),
346+ func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error ) {
347+ return azidentity .NewInteractiveBrowserCredential (& azidentity.InteractiveBrowserCredentialOptions {
348+ AuthenticationRecord : record ,
349+ Cache : cache ,
350+ })
351+ },
352+ )
353+ }
354+
355+ func cachedDeviceCodeCredentials () (azcore.TokenCredential , error ) {
356+ cacheDir , err := sopsCacheDir ()
357+ if err != nil {
358+ return nil , err
359+ }
360+
361+ return cacheTokenCredential (
362+ filepath .Join (cacheDir , cachedDeviceCodeAuthRecordFileName ),
363+ func (cache azidentity.Cache , record azidentity.AuthenticationRecord ) (CachableTokenCredential , error ) {
364+ return azidentity .NewDeviceCodeCredential (& azidentity.DeviceCodeCredentialOptions {
365+ AuthenticationRecord : record ,
366+ Cache : cache ,
367+ })
368+ },
369+ )
370+ }
0 commit comments