From 163c149f3f6ee6740ddc41532e109c7da9be99d7 Mon Sep 17 00:00:00 2001 From: Vikas Bhansali <64532198+vibhansa-msft@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:27:07 +0530 Subject: [PATCH] azureblob,azurefiles: add support for client assertion based authentication --- backend/azureblob/azureblob.go | 32 ++++++++++++++++++++++++++++++++ backend/azurefiles/azurefiles.go | 32 ++++++++++++++++++++++++++++++++ docs/content/azureblob.md | 10 ++++++++++ docs/content/azurefiles.md | 10 ++++++++++ 4 files changed, 84 insertions(+) diff --git a/backend/azureblob/azureblob.go b/backend/azureblob/azureblob.go index 2185e2cfc..31abcae54 100644 --- a/backend/azureblob/azureblob.go +++ b/backend/azureblob/azureblob.go @@ -984,6 +984,38 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e if err != nil { return nil, fmt.Errorf("failed to acquire MSI token: %w", err) } + case opt.ClientID != "" && opt.Tenant != "" && opt.MSIClientID != "": + // Workload Identity based authentication + var options azidentity.ManagedIdentityCredentialOptions + options.ID = azidentity.ClientID(opt.MSIClientID) + + msiCred, err := azidentity.NewManagedIdentityCredential(&options) + if err != nil { + return nil, fmt.Errorf("failed to acquire MSI token: %w", err) + } + + getClientAssertions := func(context.Context) (string, error) { + token, err := msiCred.GetToken(context.Background(), policy.TokenRequestOptions{ + Scopes: []string{"api://AzureADTokenExchange"}, + }) + + if err != nil { + return "", fmt.Errorf("failed to acquire MSI token: %w", err) + } + + return token.Token, nil + } + + assertOpts := &azidentity.ClientAssertionCredentialOptions{} + f.cred, err = azidentity.NewClientAssertionCredential( + opt.Tenant, + opt.ClientID, + getClientAssertions, + assertOpts) + + if err != nil { + return nil, fmt.Errorf("failed to acquire client assertion token: %w", err) + } case opt.UseAZ: var options = azidentity.AzureCLICredentialOptions{} f.cred, err = azidentity.NewAzureCLICredential(&options) diff --git a/backend/azurefiles/azurefiles.go b/backend/azurefiles/azurefiles.go index 5bb7a6da8..cb9dd2793 100644 --- a/backend/azurefiles/azurefiles.go +++ b/backend/azurefiles/azurefiles.go @@ -569,6 +569,38 @@ func newFsFromOptions(ctx context.Context, name, root string, opt *Options) (fs. if err != nil { return nil, fmt.Errorf("failed to acquire MSI token: %w", err) } + case opt.ClientID != "" && opt.Tenant != "" && opt.MSIClientID != "": + // Workload Identity based authentication + var options azidentity.ManagedIdentityCredentialOptions + options.ID = azidentity.ClientID(opt.MSIClientID) + + msiCred, err := azidentity.NewManagedIdentityCredential(&options) + if err != nil { + return nil, fmt.Errorf("failed to acquire MSI token: %w", err) + } + + getClientAssertions := func(context.Context) (string, error) { + token, err := msiCred.GetToken(context.Background(), policy.TokenRequestOptions{ + Scopes: []string{"api://AzureADTokenExchange"}, + }) + + if err != nil { + return "", fmt.Errorf("failed to acquire MSI token: %w", err) + } + + return token.Token, nil + } + + assertOpts := &azidentity.ClientAssertionCredentialOptions{} + cred, err = azidentity.NewClientAssertionCredential( + opt.Tenant, + opt.ClientID, + getClientAssertions, + assertOpts) + + if err != nil { + return nil, fmt.Errorf("failed to acquire client assertion token: %w", err) + } default: return nil, errors.New("no authentication method configured") } diff --git a/docs/content/azureblob.md b/docs/content/azureblob.md index 9e8970438..a08900c44 100644 --- a/docs/content/azureblob.md +++ b/docs/content/azureblob.md @@ -297,6 +297,16 @@ be explicitly specified using exactly one of the `msi_object_id`, If none of `msi_object_id`, `msi_client_id`, or `msi_mi_res_id` is set, this is is equivalent to using `env_auth`. +#### Fedrated Identity Credentials + +If these variables are set, rclone will authenticate with fedrated identity. + +- `tenant_id`: tenant_id to authenticate in storage +- `client_id`: client ID of the application the user will authenticate to storage +- `msi_client_id`: managed identity client ID of the application the user will authenticate to + +By default "api://AzureADTokenExchange" is used as scope for token retrieval over MSI. This token is then exchanged for actual storage token using 'tenant_id' and 'client_id'. + #### Azure CLI tool `az` {#use_az} Set to use the [Azure CLI tool `az`](https://learn.microsoft.com/en-us/cli/azure/) diff --git a/docs/content/azurefiles.md b/docs/content/azurefiles.md index a08e8d9ea..fe28662e3 100644 --- a/docs/content/azurefiles.md +++ b/docs/content/azurefiles.md @@ -295,6 +295,16 @@ be explicitly specified using exactly one of the `msi_object_id`, If none of `msi_object_id`, `msi_client_id`, or `msi_mi_res_id` is set, this is is equivalent to using `env_auth`. + +#### Fedrated Identity Credentials + +If these variables are set, rclone will authenticate with fedrated identity. + +- `tenant_id`: tenant_id to authenticate in storage +- `client_id`: client ID of the application the user will authenticate to storage +- `msi_client_id`: managed identity client ID of the application the user will authenticate to + +By default "api://AzureADTokenExchange" is used as scope for token retrieval over MSI. This token is then exchanged for actual storage token using 'tenant_id' and 'client_id'. #### Azure CLI tool `az` {#use_az} Set to use the [Azure CLI tool `az`](https://learn.microsoft.com/en-us/cli/azure/)