I'm going to answer my question. The short answer is to use
IConfidentialClientApplication.AcquireTokenOnBehalfOf(
IEnumerable<string> scopes,
UserAssertion userAssertion);
A user token acquired interactively can be used as UserAssertion.
The long version, since I'm a novice user, I'll go through all the details I've found out. It turns out there are bits here and there to create a complete runnable .net application, so not everything is directly related to my question.
1. Create an application registration in Azure AD.
Platform: Mobile and desktop application
Setup Certifiates or Secrests: We are going to use secret in this demo.
Redirect URI: add a new one under Mobile and desktop application
section, and set it as http://127.0.0.1
If running as a console application, there is no window directly assoicated to running application, so the easiest way to perform user log-in is to use system default web browser application. Thus the only way for the returned code to consumed is to use redir URL in terms of "http://localhost" or "http://127.0.0.1", aka loopback URL. In run-time, a dynamic port will be used by MSAL as a local webserver to catch the redir URL call from the web browser. Since it is running at local, both http:// or https:// are allowed, unless some one hijacked "localhost" by using DNS or hosts file.
Set-up an API in "Expose an API" section, and added a scope.
In On-Behalf-Of workflow, the user is sign-in using the scope provided by the app instead of directly access the key vault resource. We need to "Set" the Application ID URI, and create at least one scope to be used by interactive sign-in.
2. Create the key vault, and set up access policies in data plane.
Create a key vault.
In "Access Policies", Add new access policy.
To create a Compound Identity, select a valid user or group account for Seelct principal
, and select the same app we created in previous step for Authorized application
.
3. Write code
Create a .NET core console application. Add following nuget packages
<PackageReference Include="Microsoft.Identity.Client" Version="4.18.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.8" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" />
Here is the code using compound identity to acccess the key vault
const string AppClientId = "[Enter_your_Application_(client)_ID";
const string AppClientSecret = "[Enter_your_Application_(secret)";
const string TenantId = "[Enter_your_tenantId]";
const string KeyVaultBaseUri = "https://[your_keyvault_name].vault.azure.net/";
// In on-behalf-of flow, the following scope needs to be consented when acquiring the user token. Otherwise, the app cannot access the key vault on-behalf-of user.
const string KeyVaultUserImScope = "https://vault.azure.net/user_impersonation";
// In on-behalf-of flow, the following scope is used to access key vault data when acquiring client token
const string KeyVaultScope = "https://vault.azure.net/.default";
// An "Exposed API" in app registration is required when using on-behalf-of flow.
const string AppClientScope = "[Enter_your_Application_ID_URI]/[Enter_Your_Scope_Name]";
const string Instance = "https://login.microsoftonline.com/";
Console.WriteLine("Acquire User token");
var pubClient = PublicClientApplicationBuilder.Create(AppClientId)
.WithAuthority($"{Instance}{TenantId}")
.WithRedirectUri("http://localhost") // Make sure the "http://localhost" is added and selected as the app Redirect URI
.Build();
var userResult= pubClient
.AcquireTokenInteractive(new[] {AppClientScope })
.WithExtraScopesToConsent(new [] {KeyVaultUserImScope})
.WithPrompt(Prompt.Consent)
.ExecuteAsync().Result;
// In normal case, when user token is directly given from outside, we should validate if the user Result has consented to the required customized scope AppClientScope before proceeded with next steps. Here we will ignore this step.
Console.WriteLine("Acquire Client token");
// The following two steps are equivalent to https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#middle-tier-access-token-request
var conClient = ConfidentialClientApplicationBuilder.Create(AppClientId)
.WithAuthority($"{Instance}{TenantId}")
.WithClientSecret(AppClientSecret)
.Build();
var OboResult= conClient.AcquireTokenOnBehalfOf(
new[] {KeyVaultScope},
new UserAssertion(userReult.AccessToken))
.ExecuteAsync().Result;
Console.WriteLine("Access Key Vault");
var kc = new KeyVaultCredential((authority, resource, scope) =>
{
Console.WriteLine($"Authority: {authority}, Resource: {resource}, Scope: {scope}");
return Task.FromResult(OboResult.AccessToken);
});
var kvClient = new KeyVaultClient(kc);
var secretBundle = await kvClient.GetSecretAsync(KeyVaultBaseUri, SecretName);
Console.WriteLine("Secret:" + secretBundle.Value);
If we are not using compound identity, we can use Azure.Security.KeyVault.Secrets.SecretClient
to access the key vault data by one of the following method
// For access policy assigned to confidential application
var client = new SecretClient(new Uri(KeyVaultBaseUri),
new ClientSecretCredential(TenantId, AppClientId, AppClientSecret));
var secretBundle = await client.GetSecretAsync(SecretName);
Console.WriteLine("Secret:" + secretBundle.Value.Value);
and
// For access policy assigned to User or Group account
var client = new SecretClient(new Uri(KeyVaultBaseUri), new InteractiveBrowserCredential());
var secretBundle = await client.GetSecretAsync(SecretName);
Console.WriteLine("Secret:" + secretBundle.Value.Value);