Azure AD Access token of Client Application does not contain Roles Claims - oauth-2.0

I have 2 Azure AD applications say Client-App and Server-App in Azure AD App registrations.
Server AD Application:
Registered a new App in Azure AD.
Set up App Roles with name "Search.Address" which is custom role.
Client AD Application:
Registered a new App in Azure AD.
API Permissions: Added the role "Search.Address" created in server-app registration is exposed as an Application Permissions in client app.
Granted Admin access successfully.
I have client Function App created with .NET stack and enabled system managed identity which is associated with Client-App. Client function app runs code to get an access token using ManagedIdentityCredential.
Token is successfully created but role "Search.Address" is missing.
I tried Client-App exposing as an API. But in no vain.
Does Managed identity have any permission to talk to server? How I can assign that using approleassignment ?
public static class Function1
{
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
ILogger log)
{
log.LogInformation("C# HTTP trigger function processed a request.");
string name = req.Query["name"];
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;
string responseMessage = string.Empty;
try
{
var credential = new ManagedIdentityCredential();
var accessToken = await credential.GetTokenAsync(new TokenRequestContext(scopes: new string[] { "SERVERAPP_ClientID/.default" }) { });
responseMessage = string.IsNullOrEmpty(name)
? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
: $"Hello, {name}. Your Token: {accessToken.Token}";
}
catch (Exception ex)
{
log.LogError(ex.Message+ex.InnerException.Message);
}
return new OkObjectResult(responseMessage);
}
}
Reference:
https://anoopt.medium.com/few-ways-of-obtaining-access-token-in-azure-application-to-application-authentication-40a9473a2dde

You need to assign the application permission/app role to the Managed Identity service principal.
You can't do this through the Portal at the moment, you'll need PowerShell to do that:
Connect-AzureAD
New-AzureADServiceAppRoleAssignment -ObjectId 1606ffaf-7293-4c5b-b971-41ae9122bcfb -Id 32028ccd-3212-4f39-3212-beabd6787d81 -PrincipalId 1606ffaf-7293-4c5b-b971-41ae9122bcfb -ResourceId c3ccaf5a-47d6-4f11-9925-45ec0d833dec
Note that this is for the older AzureAD module. There is also a similar cmdlet for the newer Microsoft.Graph module.
For the AzureAD cmdlet, the ids you need are:
ObjectId and PrincipalId: Managed Identity Service Principal object ID
Id: id of the app role/app permission
ResourceId: object ID of the API Service Principal
Running this command is the same thing as the admin consent for application permissions.
Article I wrote on this: https://joonasw.net/view/calling-your-apis-with-aad-msi-using-app-permissions

Related

Which token should I use in my React app call backend API , which forward call to Graph API

In my React app I use OAuth 2.0 auth code grant for authentication , and use Graph APi to make most of the requests (I have an app registered in Azure AD). They work perfectly with the access token I received using MSAL2 Provider (toolkit).
Providers.globalProvider = new Msal2Provider({
clientId: config.clientId,
authority: config.authority,
redirectUri: config.redirectUri,
scopes: [
"openid",
"profile",
"Calendars.ReadWrite",
"Place.ReadWrite",
"Place.ReadWrite.All",
"User.Read",
"People.Read",
],
});
I get the access token with following function:
const getAccessToken = async () => {
let token;
if (Providers.globalProvider.state === ProviderState.SignedIn) {
token = await Providers.globalProvider.getAccessToken();
token = `Bearer ${token}`;
}
return token;
};
Recently I got another service to do but not directly to the Graph API but by our Backend who after some validation make a query to Graph API ( exactly its a room update call
https://graph.microsoft.com/v1.0/places/${roomId}
My service :
const updateRoom=async (roomId:string|undefined, room:Room):Promise<void>=> {
return axios.put(`http://localhost:3000/api/v1/rooms/${roomId}`, {capacity:Number(room.capacity),equipments:room.tags},{
headers: {
Authorization: await getAccessToken(),
},
})
But what I get from the server is status 500, AxiosError: Request failed with status code 500\n.
When testing our backend app in Swagger- > it works , the room is updated correctly. I have tried to copy the token from the Swagger and use the same in my React app--> also works , but I can not use for this purpose the access token I receive from MSAL2. Why does it happen? Where to find a solution either on the front or backend?
Are you using the same account to test request in Swagger and React app? As docs say only "work or school account" is supported for Place update. can it be an issue?
Firstly, you have a backend api which required to be called by your react client app. Then your backend api also required to call ms graph api to update the room. So I deduce this document matched your scenario.
Then I have a question here, how your backend web app to call the ms graph api? There are 2 possibilities, one is that it calls ms graph api on behalf the application-self, which should use client credential flow. Another one is that it calls ms graph api on behalf of the signed-in user which required to use on-behalf-of flow.
When using client credential flow, it's not necessary to set microsoft authentication for your backend app, it depends on yourself to add authentication or not. And if you called this api, then it doesn't support application type of api permissin, then you are impossible to use client credential flow here.
Let's focus on the first scenario. The whole progress should look like this: your react client app generate an access token and use it to call your backend api --> your backend api validate the token and try to call graph api on behalf of the signed in user. In this scenario, you should protect your backend api with Azure AD first, so that it can managed by Azure AD authorization. Then using On-behalf-flow allows you to generate another access token with the access token send from react client app and use it to call ms graph api.
I have a sample with asp.net core MVC(represent the client app) and and asp.net core web api. In my MVC project, I generate an access token for calling the api.
[Authorize]
public async Task<string> GetAsync() {
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "api://aad_client_id_exposing_api/Tiny.Read" });
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get,"https://localhost:7212/WeatherForecast")
{
Headers =
{
{ HeaderNames.Authorization, "Bearer "+ accessToken}
}
};
var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.SendAsync(httpRequestMessage);
var res = "";
if (response.StatusCode == HttpStatusCode.OK)
{
res = await response.Content.ReadAsStringAsync();
}
return "hello" + res;
}
Pls note the scope I used above, it's not for graph api, but an api permission which exposed by ourself, more details can be seen here.
Then this is my web api. In program.cs and appsettings.json, I have the following to protect my api with Azure AD, with the nuget package Microsoft.Identity.Web.MicrosoftGraph:
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "aad_client_app_id",
"ClientSecret": "client_secret",
"Domain": "tenant_name",
"TenantId": "tenant_id" // A guid (Tenant ID = Directory ID) or 'common' or 'organizations' or 'consumers'
//"Audience": "api://aad_app_client_id_which_exposed_api" //I used the same aad client app to expose api and consent api permission, so I can comment this line
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read"
},
And this is my web api controller:
namespace WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
private readonly GraphServiceClient _graphServiceClient;
public WeatherForecastController(GraphServiceClient graphServiceClient)
{
_graphServiceClient = graphServiceClient;
}
[RequiredScope("Tiny.Read")]
[HttpGet]
public async Task<string> GetAsync()
{
var user = await _graphServiceClient.Me.Request().GetAsync();
var name = user.DisplayName;
return name;
//return "world";
}
}
}
And it worked for me. I'm afraid your issue happened for the scope you used in the client app, it should be the scope you exposed by yourself but not the one which is used for calling graph api.

Azure Key Vault Quickstart fails to provide key vault client

I am learning the Azure Key Vault. I am trying to follow https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-net. I have created a vault in the portal, assigned an access policy for a user account. I have seen the vault in the protal, and I have seen that the user has I have set the environment variable value. I use an 'az login' command at a prompt to use that account. From the prompt I run the code in the quickstart. It prompts for a secret. I enter 'bob.' It throws an exception.
"Creating a secret in [...]-key-vault called 'mySecret' with the value 'bob' ...Unhandled exception. Azure.Identity.AuthenticationFailedException: SharedTokenCacheCredential authentication failed: A configuration issue is preventing authentication - check the error message from the server for details.You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS70002: The client does not exist or is not enabled for consumers. If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908."
The exception is at
await client.SetSecretAsync(secretName, secretValue);
I think the problem is coming from
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
The client is not able to send tokens that the vault accepts. I am at a loss. I have had a couple of people with some expertise in using the Vault review this code and they haven't been able to provide insight. Any help?
Here is the code, from the example:
using System;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
namespace key_vault_console_app
{
class Program
{
static async Task Main(string[] args)
{
const string secretName = "mySecret";
var keyVaultName = Environment.GetEnvironmentVariable("KEY_VAULT_NAME");
var kvUri = $"https://{keyVaultName}.vault.azure.net";
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
Console.Write("Input the value of your secret > ");
var secretValue = Console.ReadLine();
Console.Write($"Creating a secret in {keyVaultName} called '{secretName}' with the value '{secretValue}' ...");
await client.SetSecretAsync(secretName, secretValue);
Console.WriteLine(" done.");
Console.WriteLine("Forgetting your secret.");
secretValue = string.Empty;
Console.WriteLine($"Your secret is '{secretValue}'.");
Console.WriteLine($"Retrieving your secret from {keyVaultName}.");
var secret = await client.GetSecretAsync(secretName);
Console.WriteLine($"Your secret is '{secret.Value}'.");
Console.Write($"Deleting your secret from {keyVaultName} ...");
DeleteSecretOperation operation = await client.StartDeleteSecretAsync(secretName);
// You only need to wait for completion if you want to purge or recover the secret.
await operation.WaitForCompletionAsync();
Console.WriteLine(" done.");
Console.Write($"Purging your secret from {keyVaultName} ...");
await client.PurgeDeletedSecretAsync(secretName);
Console.WriteLine(" done.");
}
}
}
Not Sure the root reason for it. But If you want to use a user account to login to Azure and access your key vault, using UsernamePasswordCredential() could be a workaround here.
To use UsernamePasswordCredential(), you should register a client application in Azure AD: Go to Azure portal =>Azure Active Directory => New registration
Note its application ID:
Go to API permissioms, and grant key vault user_impersonation permission so that users could access key vault via this app.
Click "Grant admin consent for.." to finish the permission grant process.
Go to the "Authentication" blade, turn on "Allow public client flows" so that Azure will consider this app as a public client:
Try the code below to create a secret:
using System;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
namespace key_vault_console_app
{
class Program
{
static async Task Main(string[] args)
{
const string secretName = "mySecret2";
var keyVaultName = "<your kv name>";
var kvUri = $"https://{keyVaultName}.vault.azure.net";
var userCred = new UsernamePasswordCredential("<user account name>", "<user password>", "<your tenant name/id>", "<client application ID WHCIH we created above>");
var client = new SecretClient(new Uri(kvUri), userCred);
Console.Write("Input the value of your secret > ");
var secretValue = Console.ReadLine();
Console.Write($"Creating a secret in {keyVaultName} called '{secretName}' with the value '{secretValue}' ...");
await client.SetSecretAsync(secretName, secretValue);
Console.WriteLine(" done.");
}
}
}
Result:

Missing scope in access token - code flow

I am trying to execute Oauth2 code flow to get access token but not able to fetch built-in email scope.
Below is my setup.
I have registered an application in Azure Active Directory. Lets say app id is - APP1
I am using V2 endpoint to access 'code' from 'authorize'endpoint.
Below is piece of code
[HttpPost]
public IActionResult Index(MyModel myModel)
{
HttpClient client = new HttpClient();
var authEndpoint = "https://login.microsoftonline.com/{my-tenant-id}/oauth2/v2.0/authorize?client_id=APP1&response_type=code&scope=openid+email";
return Redirect(authEndpoint);
}
public IActionResult Callback(string code, string error)
{
Console.WriteLine("callback");
AuthenticationContext context = new AuthenticationContext("https://login.microsoftonline.com/9e8754b6-f9cd-4aed-974d-a0ec0f3ed703");
ClientCredential cc = new ClientCredential("APP1", "xxxxxxx/_");
var resource = "c4887ff4-f750-4f1b-9781-744affe6fee2";
var r = context.AcquireTokenAsync(resource,cc).Result;
var tokenEndpoint = "https://login.microsoftonline.com/9e8754b6-f9cd-4aed-974d-a0ec0f3ed703/oauth2/v2.0/token";
return Ok("");
}
Note that I am requesting two scopes openid and email
I am getting callback with appropriate code which I am trading further to retrieve access token using ADAL library.
I am getting back the access token but scope is missing in the access token . Please see below snap.
You are using the wrong method on the confidential client app object. You aren't using the code value.
So you are acquiring a token through client credentials flow, which never has scopes since it is an app-only flow.
Use the method/overload that accepts an authorisation code :)

To retrieve access token

I have created a MVC application to escalate work to other person inside my organization. I have added all the members in my organization to AAD,
and registered an application there, created app service and linked that app service to registered app with SSO enabled.
Now every time someone visits the app, they can login successfully using their respective credential.
What I want to do know is to retrieve all the members in my AAD and display them inside dropdown list so that anyone can escalate to others by just looking in the dropdown list.
I have tried with sample graph SDK to get the name of users in my organization
with this code
private string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private string appId = ConfigurationManager.AppSettings["ida:AppId"];
private string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private string scopes = ConfigurationManager.AppSettings["ida:GraphScopes"];
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, httpContext).GetMsalCacheInstance();
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }), cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new ServiceException(
new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = Resource.Error_AuthChallengeNeeded,
});
}
}
with some change in scope.
<add key="ida:AppId" value="xxxxx-xxxxxxx-xxxxxx-xxxxxxx"/>
<add key="ida:AppSecret" value="xxxxxxxxxxx"/>
<add key="ida:RedirectUri" value="http://localhost:55065/"/>
<add key="ida:GraphScopes" value="User.ReadBasic.All User.Read Mail.Send Files.ReadWrite"/>
This enables me to get basic details of all user in my organization.
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Thanks
Subham, NATHCORP, INDIA
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Based on my understanding, you are using the build-in feature App Service Authentication / Authorization. You could follow here to configure your web app to use AAD login. And you need to configure the required permissions for your AD app as follows:
Note: For Azure AD graph, you need to set the relevant permissions for the Windows Azure Active Directory API. For Microsoft Graph, you need to configure the Microsoft Graph API.
Then, you need to configure additional settings for your web app. You could access https://resources.azure.com/, choose your web app and update App Service Auth Configuration as follows:
Note: For using Microsoft Graph API, you need to set the resource to https://graph.microsoft.com. Details, you could follow here.
For retrieving the access token in your application, you could get it from the request header X-MS-TOKEN-AAD-ACCESS-TOKEN. Details, you could follow Working with user identities in your application.
Moreover, you could use Microsoft.Azure.ActiveDirectory.GraphClient package for Microsoft Azure Active Directory Graph API, Microsoft.Graph package for Microsoft Graph API using the related access token.

How to get access token for web api from native app in other tenant?

I've created a multi tenant Web API that works just fine. Now I want to build a native client for testing. The Web API app is defined in one tenant (webapitenant). The test app is defined in another tenant (clienttenant) that has given admin consent to the Web API.
I've added the testClientId as a knownClientApplication in the Web API's app manifest and oauth2AllowImplicitFlow enabled. The test client has been granted permissions to the Web API app.
GetAccessToken:
var userCredential = new UserCredential("admin#clienttenant.onmicrosoft.com", "password");
var context = new AuthenticationContext("https://login.windows.net/common");
return context.AcquireToken("https://webapitenant.onmicrosoft.com/webApiResourceUri", testClientId, userCredential).AccessToken;
Exception thrown: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException' in Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Additional information: AADSTS65001: The user or administrator has not consented to use the application with ID 'nativeclientid'. Send an interactive authorization request for this user and resource.
Exception thrown: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException' in Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Additional information: AADSTS65001: The user or administrator has not consented to use the application with ID nativeclientid. Send an interactive authorization request for this user and resource.
Update
I created a dummy console app to force a consent form that I could accept. ADAL now returns tokens but my Web API rejects them (status 401).
var parameters = new PlatformParameters(PromptBehavior.Always);
var context = new AuthenticationContext("https://login.windows.net/common");
var token = context.AcquireTokenAsync
("https://webapi.onmicrosoft.com/appuri",
"testappid",
new Uri("https://webapi.azurewebsites.net"), parameters).Result.AccessToken;
Console.WriteLine(token); //Output: oauth token
var client = new HttpClient
{
BaseAddress = new Uri("https://webapi.azurewebsites.net/api/")
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync("tenants").Result;
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
// Output: {"$type":"System.Web.Http.HttpError, System.Web.Http","Message":"Authorization has been denied for this request."}
Please ensure that the web app is ignore the issue validation and the audience is same as the resource(https://webapi.onmicrosoft.com/appuri",
"testappid) you acquire for the access token and this value should be the App ID URI which you can find it on old Azure portal like figure below:
Here is the relative code for setting for the authentication of multi-tenant web API:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters= new System.IdentityModel.Tokens.TokenValidationParameters {
ValidateIssuer=false
}
});

Resources