Blazor server authorized downstream API call with Azure AD B2C - azure

I’m trying to use Microsoft.Identity.Web to call an API from my .NET 6 Blazor server app. I can successfully login to authorized pages in my Blazor app but when I try to make an API call using CallWebApiForUserAsync I get an inner exception of “No account or login hint was passed to the AcquireTokenSilent call”.
The couple of posts here on Stack Overflow and the MS documentation look to me like I have it configured correctly. What am I missing?
appsettings.json:
"AzureAd": {
"Authority": "https://hivercom.b2clogin.com/XXXXXX.onmicrosoft.com/B2C_1_SignUpSignIn",
"Instance": "https://XXXXXX.b2clogin.com",
"TenantId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"ClientId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"CallbackPath": "/signin-oidc",
"Domain": "XXXXXX.onmicrosoft.com",
"SignedOutCallbackPath": "/signout/B2C_1_susi",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "B2C_1_EditProfile",
"ClientSecret": "XXXXXXXXXXXXXXXXXX"
},
"HiverAPI": {
"BaseUrl": "https://localhost:7075",
"Scopes": "https://XXXXXX.onmicrosoft.com/XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX/all"
},
Program.cs
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using BlazorB2C.Data;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://XXXXXX.onmicrosoft.com/XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX/all" })
.AddDownstreamWebApi("HiverService", builder.Configuration.GetSection("HiverAPI"))
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
Level.razor:
#page "/level"
#using Microsoft.Identity.Web
#inject IHttpClientFactory HttpClientFactory
#inject Microsoft.Identity.Web.ITokenAcquisition TokenAcquisitionService
#inject Microsoft.Identity.Web.IDownstreamWebApi DownstreamWebApi
#inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
#attribute [Authorize]
#attribute [AuthorizeForScopes(ScopeKeySection = "HiverAPI:Scopes")]
<PageTitle>Level</PageTitle>
Log out
<h3>Level</h3>
#code {
private const string ServiceName = "HiverService";
protected override async Task OnInitializedAsync()
{
try
{
error >> string token = await TokenAcquisitionService.GetAccessTokenForUserAsync(new string[] { "https://XXXXXX.onmicrosoft.com/XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX/all" });
error >> var value = await DownstreamWebApi.CallWebApiForUserAsync(
ServiceName,
options =>
{
options.RelativePath = $"HiverUser";
});
}
catch (Exception ex)
{
var message = ex.Message;
ConsentHandler.HandleException(ex);
}
try
{
error >> await DownstreamWebApi.CallWebApiForUserAsync(
ServiceName,
options =>
{
options.HttpMethod = HttpMethod.Put;
options.RelativePath = $"HiverUser/0fd30e94-a116-4ef0-b222-4125546b8561";
});
}
catch (Exception ex)
{
var message = ex.Message;
ConsentHandler.HandleException(ex);
}
}
}
Azure configuration for client app:
Azure configuration for API

CallWebApiForAppAsync uses the on-behalf flow, which is not available for Azure AD B2C.
https://github.com/AzureAD/microsoft-identity-web/wiki/b2c-limitations
You cannot use ITokenAcquisition.GetTokenForAppAsync or IDownstreamApi.CallWebApiForAppAsync in Azure AD B2C web apps.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens
As an alternative, you can request access tokens for downstream APIs(Hiver.API in your case) using GetAccessTokenForUserAsync. The resulting access token can be used to call the API. You'll need to use a standard HttpClient instead of IDownstreamWebApi. Full example can be found here:
https://github.com/Azure-Samples/ms-identity-blazor-server/blob/main/WebApp-your-API/B2C/README-Incremental.md
Relevant code from Github:
private async Task PrepareAuthenticatedClient()
{
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { _TodoListScope });
Debug.WriteLine($"access token-{accessToken}");
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

Related

Swagger authenticate Azure AD B2C redirect url

I could authenticate swagger on Azure AD B2C. It works fine on http://localhost:5078/ . But as soon as I push my website to azure I can not authenticate swagger. It still uses localhost:5078 as redirect URL and not azure website URL.
My code:
builder.Services.AddSwaggerGen(c => {
var OATH2 = "oauth2";
// Enabled OAuth security in Swagger. From here: https://stackoverflow.com/questions/66894523/swagger-not-able-to-authenticate-to-azure-ad-b2c
c.AddSecurityRequirement(new OpenApiSecurityRequirement() {
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = OATH2
}
},
new List<string>()
}
});
c.AddSecurityDefinition(OATH2, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{builder.Configuration["AzureAdB2C:Instance"]}/{builder.Configuration["AzureAdB2C:Domain"]}/{builder.Configuration["AzureAdB2C:SignUpSignInPolicyId"]}/oauth2/v2.0/authorize"),
TokenUrl = new Uri($"{builder.Configuration["AzureAdB2C:Instance"]}/{builder.Configuration["AzureAdB2C:Domain"]}/{builder.Configuration["AzureAdB2C:SignUpSignInPolicyId"]}/oauth2/v2.0/token"),
Scopes = new Dictionary<string, string>() { { builder.Configuration["AzureAdB2C:Scope"], "backend all" } }
}
}
});
});
....
app.UseSwagger();
app.UseSwaggerUI(c => {
c.OAuthClientId(builder.Configuration["AzureAdB2C:SwaggerClientId"]);
c.OAuthScopes(builder.Configuration["AzureAdB2C:Scope"]);
c.OAuthUsePkce();
});
OK on localhost:
Not OK on azure:
https://anna-carat-auth-test.azurewebsites.net/swagger/index.html
Both redirect URLs are registered in app:
How to set a proper redirect URL for swagger?
Created a sample Web Api application.
In Visual Studio click on connected services and add a dependency of Microsoft Identity platform as shown in below snippet.
Once the dependencies are added then in Program.cs file is updated with Azure AD related settings.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// It added the Authentication and Authorization methods.
app.UseAuthentication();
app.UseAuthorization();
In Program.cs file made the code changes as shown below.
builder.Services.AddSwaggerGen(
x =>
{
x.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "Swagger Azure AD Demo",
Version = "v1"
});
x.AddSecurityDefinition("oauth2", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = "Oauth2.0 which uses AuthorizationCode flow",
Name = "oauth2.0",
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(builder.Configuration["SwaggerAzureAD:AuthorizationUrl"]),
TokenUrl = new Uri(builder.Configuration["SwaggerAzureAD:TokenUrl"]),
Scopes = new Dictionary<string, string>
{
{builder.Configuration["SwaggerAzureAD:Scope"], "Access API as User" }
}
}
}
});
x.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference= new OpenApiReference{Type=ReferenceType.SecurityScheme,Id="oauth2"}
},
new[]{builder.Configuration["SwaggerAzureAD:Scope"]}
}
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(x=>
{
x.OAuthClientId(builder.Configuration["SwaggerAzureAD:ClientId"]);
x.OAuthUsePkce();
});
}
And the AppSettings.Json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "Something.onmicrosoft.com",
"TenantId": "TenantID",
"ClientId": "ClientID",
"CallbackPath": "/signin-oidc",
"Scopes": "access_as_user"
},
// These values need to be copied from App registartion
"SwaggerAzureAD": {
"AuthorizationUrl": "",
"TokenUrl": "",
"ClientId": "",
"Scope": ""
}
}
Expose API from App registration.
API Permissions need to be given in the App registrations of Azure.
And has to be copied the value and need to use in the scope attribute of AppSettings.json
And redirection urls to be mentioned and to be used in AppSettings.json form App registration -> Authentication as shown in below snippet.
And able to get the response without any issues.
Response:

Azure AD C2C/Next.Js/ASP.NET Core 6: How to use PKCE auth code flow access tokens to access Web API?

I need help, I got stuck for a few days trying to protect a Web API (ASP.NET Core 6) by sending access token from users (Azure Ad B2C with google provider/local account) signed in with authentication code flow (PKCE).
This generates a (401) unauthorized error. What is going wrong?
The Next.js Application: (helloworld.webapp)
Azure Ad B2C settings
Manage -> Authentication
Single-page-application with redirect uri: http://localhost:3000
Manage -> API permissions
helloworld.api
access_as_user | admin consent required: yes | status: ✅ Granted for helloworld
Microsoft Graph
offline_access | admin consent required: yes | status: ✅ Granted for helloworld
openid | admin consent required: yes | status: ✅ Granted for helloworld
(unchecked)
🔲 Access tokens (used for implicit flows)
🔲 ID tokens (used for implicit and hybrid flows)
This is how you log in.
const login = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("only logs in browser console")
instance.loginRedirect({
scopes: [
"openid",
"offline_access",
// ✅ checked for typo
"https://helloworld.onmicrosoft.com/8a1bc000-2000-4000-a000-d3156a663000/access_as_user",
]
})
}
In the following snippet you can see how I get my accessToken.
const postData = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
// to prevent ssr mismatch errors.
if (!data && inProgress === InteractionStatus.None) {
instance
.acquireTokenSilent({
scopes: [
"openid",
"offline_access",
// ✅ checked for typo
"https://helloworld.onmicrosoft.com/8a1bc000-2000-4000-a000-d3156a663000/access_as_user",
]
})
.then((accessTokenResponse) => {
let accesstoken = accessTokenResponse.accessToken;
console.log(accesstoken); // 👍only logs in browser console.
// // ✅ weatherforecast endpoint checked. Maps to http://localhost:5015/api/:path*
fetch("/api/net/weatherforecast", {
method: "GET",
headers: {
Authorization: `Bearer ${JSON.stringify(accesstoken)}`,
},
})
.then((res) => console.log(res))
.catch((err) => console.log(err));
});
}
};
I have the following rule in next.config.js
async rewrites() {
return {
fallback: [
{
source: "/api/net/:path*",
destination: "http://localhost:5015/api/:path*",
},
],
};
},
ASP.NET Core 6 application: (helloworld.api)
manage -> expose API
scopes ✅ all checked for typo
https://helloworld.onmicrosoft.com/8a1bc000-2000-4000-a000-d3156a663000/access_as_user
Who can consent: Admins only
State: Enabled
The appsettings.json
"AzureAdB2C": {
"Instance": "https://helloworld.b2clogin.com/",
"ClientId": "8a1bc000-2000-4000-a000-d3156a663000",
"Domain": "helloworld.onmicrosoft.com",
"Scopes": "access_as_user",
"SignUpSignInPolicyId": "B2C_1_authflow"
},
The Program.cs generated with dotnet new webapi -au IndividualB2C with added cors.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors(o => o.AddPolicy("default", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseHttpsRedirection();
}
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The WeatherForecastController
[Authorize]
[ApiController]
[Route("api/[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
And lastly, the user flow
User Flow (B2C_1_authflow)
Possible causes?:
no https as this gave a net::ERR_CERT_AUTHORITY_INVALID
else
{
app.UseHttpsRedirection();
}
Some problem because next.js is a full-stack application? I believe it acts as SPA as I delay the acquiring accessToken untill InteractionStatus.None?
Please check if you need to modify the way of provisioning scope in app settings. Try giving complete scope https://<tenant>/api/<scope>.
{
"AzureAdB2C": {
"Instance": "https://<your-tenant-name>.b2clogin.com",
"ClientId": "<web-app-application-id>",
"Domain": "<your-b2c-domain>",
"SignUpSignInPolicyId": "<your-sign-up-in-policy>"
// "SignedOutCallbackPath": "/signout/<your-sign-up-in-policy>",
},
"TodoList": {
"TodoListScope": "https://contoso.onmicrosoft.com/api/access_as_user openid offline_access",
"TodoListBaseAddress": "https://localhost:44332"
}
}
Also check if app.UseHttpsRedirection(); is to be used after if loop
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
Also please make sure to recheck and give proper redirect url , which has to be same in portal and authorization request. It has to be valid url
Reference:
Configure authentication in a sample web application that calls a web API by using Azure Active Directory B2C | Microsoft Docs

MSAL access token getting unauthorized with MicrosoftIdentityWebApi Authentication

I set up my Startup Class below:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
My AzureAd appsettings.json is below:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "2f0d9252-e207-4d7f-b4da-*******",
"TenantId": "de1ef02c-7cfa-46b8-a02b-*******",
"Audience": "2f0d9252-e207-4d7f-b4da-*******"
}
Now my controller below:
[HttpGet]
public async Task<string> Get()
{
//To be put on appsettings.json
string clientId = "2f0d9252-e207-4d7f-b4da-0cc618e77c93";
string tenantId = "de1ef02c-7cfa-46b8-a02b-61ab78bc602b";
var app = PublicClientApplicationBuilder.Create(clientId)
.WithRedirectUri("http://localhost:5000")
.WithTenantId(tenantId)
.Build();
string[] scopes = new string[] { };
//Azure Login Success here
var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
//Azure service graph get success.
string graphResult = await "https://graph.microsoft.com/beta/me"
.WithOAuthBearerToken(result.AccessToken)
.GetStringAsync();
//I pass the AccessToken from the result. But I'm getting UnAuthorized.
string authorizeResult = await "https://localhost:44328/weatherforecast/AuthorizeGet"
.WithOAuthBearerToken(result.AccessToken)
.GetStringAsync();
return graphResult;
}
[Authorize]
[HttpGet]
public async Task<string> AuthorizeGet()
{
return "Authorize";
}
The Azure Login will success here. But after I get the token and request to AuthorizeGet Api. It will give me Unauthorized 401.
Anything I missed out for the configuration?
Below is my Directory Authentication config.

Azure Service App - configure startup host to use bearer token

I want to host my REST API on Azure Service App with authorization bearer token.
On Azure portal I've done:
Create new App Service (testservice).
On Authentication/Authorization:
App Service Authentication -> On
Action to take when request is not authenticated -> Log in with Azure Active Directory. Set Authentication Providers to Active Directory Authentication with Management mode: Express and create new Azure AD Application (register new app: testserviceapp)
Token Store -> On
On App registrations -> testserviceapp:
Authentication -> Web -> Redirect URIs: https://testservice.azurewebsites.net/.auth/login/aad/callback
Authentication -> Web -> Implicit grant -> Access tokens: true, ID tokens: true
Authentication -> Supported account types -> Accounts in this organizational directory only (... - Single tenant)
Authentication -> Advanced settings -> Treat application as a public client: No
Certificates & secrets -> create new ClientSecret
Expose an API -> Add a scope: https://testservice.azurewebsites.net/user_impersonation
Expose an API -> Add a scope: https://testservice.azurewebsites.net/WeatherForecast
Next, I created RestApiService project in VisualStudio and publish into Azure.
appsettings.json in my RestApiService host:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"AzureAd": {
"ClientId": "xxx",
"Domain": "xxx.onmicrosoft.com",
"Instance": "https://login.microsoftonline.com/",
"TenantId": "xxx",
"CallbackPath": "/signin-oidc",
"ClientSecret": "xxx",
"AppIDURL": "https://testservice.azurewebsites.net",
"ConfigView": "API"
}
}
WeatherForecastController.cs:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme).AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
On client app I successfully get AccessToken:
static AuthToken GetToken()
{
var uri = "https://login.microsoftonline.com/";
var method = $"{TenantID}/oauth2/token";
var client = new RestClient(uri);
var request = new RestRequest($"{uri}{method}", Method.POST);
request.AddParameter("grant_type", "client_credentials");
request.AddParameter("client_id", ClientID);
request.AddParameter("client_secret", ClientSecret);
request.AddParameter("resource", "https://testservice.azurewebsites.net/");
return client.Execute<AuthToken>(request).Data;
}
But when I try to call api with this token, I receive 401 Unauthorized error (You do not have permission to view this directory or page.):
static void GetItems(string token)
{
var client = new RestClient("https://testservice.azurewebsites.net/");
var request = new RestRequest("https://testservice.azurewebsites.net/WeatherForecast", Method.GET);
request.AddHeader("Authorization", $"bearer {token}");
var response = client.Execute(request);
}
What am I doing wrong?

.Net core App unable to get user details from Microsoft Graph API?

Net core Web API project. I registered app in azure AD for Web API app. I configured swagger and I registered one more app in Azure AD.
I am trying to do authorization on my webapis based on groups. In appsettings.json I have all the values.
Below is my startup looks like.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddAuthentication(o =>
{
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = azureActiveDirectoryOptions.Authority;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new List<string>
{
azureActiveDirectoryOptions.AppIdUri,
azureActiveDirectoryOptions.ClientId
},
ValidateIssuer = true
};
});
services.AddScoped<IAuthorizationHandler, GroupsCheckHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("GroupsCheck", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new GroupsCheckRequirement("2a39995a-8fd1-410e-99e2-11cf6046090d"));
});
});
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = swaggerUIOptions.AuthorizationUrl,
TokenUrl = swaggerUIOptions.TokenUrl
});
c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new[] { "readAccess", "writeAccess" } }
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(swaggerUIOptions.ClientId);
c.OAuthClientSecret(swaggerUIOptions.ClientSecret);
c.OAuthRealm(azureActiveDirectoryOptions.ClientId);
c.OAuthAppName("Swagger");
c.OAuthAdditionalQueryStringParams(new { resource = azureActiveDirectoryOptions.ClientId });
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseAuthentication();
app.UseMvc();
}
When I run the application using https://localhost:44319/swagger
Now I have Authorize button in my swagger. Whenever I try to Authorize, It will ask me to enter user name and password. Authentication works as expected. Next I want to hit /api/values/users/{id}. the controller looks below.
[Authorize(Policy = "GroupsCheck")]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
}
I need group based authorization. In startup I have added policy.
services.AddAuthorization(options =>
{
options.AddPolicy("GroupsCheck", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new GroupsCheckRequirement("2a39995a-8fd1-410e-99e2-11cf6046090d"));
});
});
Below is my GroupsCheckHandler.cs
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
GroupsCheckRequirement requirement)
{
GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
//Tried to get user and dint work for me
var user = await client.Me.Request().GetAsync();
//Here exception occurs
var groupList = await client.Groups.Request().GetAsync();
var result = false;
foreach (var group in groupList)
{
if (requirement.groups.Equals(group.Id))
{
result = true;
}
}
if (result)
{
context.Succeed(requirement);
}
}
Below is my MicrosoftGraphClient.cs
public static async Task<GraphServiceClient> GetGraphServiceClient()
{
// Get Access Token and Microsoft Graph Client using access token and microsoft graph v1.0 endpoint
var delegateAuthProvider = await GetAuthProvider();
// Initializing the GraphServiceClient
graphClient = new GraphServiceClient(graphAPIEndpoint, delegateAuthProvider);
return graphClient;
}
private static async Task<IAuthenticationProvider> GetAuthProvider()
{
AuthenticationContext authenticationContext = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
// ADAL includes an in memory cache, so this call will only send a message to the server if the cached token is expired.
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(graphResource, clientCred).ConfigureAwait(false);
var token = authenticationResult.AccessToken;
var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.ToString());
return Task.FromResult(0);
});
return delegateAuthProvider;
}
Now Whenever I start hitting to my api am getting exception in groupshandler.cs
Microsoft.Graph.ServiceException: Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.
I have added Microsoft graph permission to my app in azure AD. I thing to read groups we need Admin consent. Here I am struggling. Below permission I can see under enterprise application in azure ad under user consent tab.
Below is token format generated through authenticationContext.AcquireTokenAsync method
Other side this token also looks strange to me and this is missing many fields.
Now someone please help me what wrong steps I have done in the above implementation. Can someone help in this regard. Any help would be very helpful for me. Thanks a lot
You were using client credential to get the access Token. So you need to add application permissions(not delegated permissions) on Azure portal.
After adding the application permissions, you also need to grant admin consent.

Resources