I want to use IdentityServer with my ServiceStack API, however, when I add this in my Startup.cs in this method: public override void Configure(Container)
Plugins.Add(new IdentityServerAuthFeature
{
AuthProviderType = IdentityServerAuthProviderType.UserAuthProvider,
AuthRealm = "XXX",
ClientId = "YYY",
ClientSecret = "ZZZ"
});
And when I wrap my class \ method with [Authenticate] and try to access it I'm getting this response:
{
"responseStatus": {
"errorCode": "Exception",
"message": "No registered Auth Providers found matching IdentityServer provider"
}
}
Am I missing something?
Here's the actual implementation of this IdentityServerAuthFeature
EDIT - SOLUTION
After some debugging the problem was with HostConfig. I forgot to add WebHostUrl and I saw that while I was looking later on metadata (?debug=requestinfo), the error was:
ConfigurationException
appHost.Config.WebHostUrl must be set to use the Identity Server User Login >plugin so that the service can sent it's full http://url:port to the >Identity Server User Login
Related
I am to build an ASP.NET Core webapp with .NET6.
I have followed the Azure-Sample repository on GitHub
I have adjusted the appsettings.json with AzureAd section:
"AzureAd": {
"ClientId": "my-client-guid",
"TenantId": "my-tenant-guid",
"Domain": "my-tenant-name",
"ClientSecret": "my-client-secret-guid",
"Instance": "https://login.microsoftonline.com/",
"Scopes": "user.read,profile,openid",
"ClientCapabilities": [ "cp1" ],
"CallbackPath": "/signin-oidc"
}
ClientId and TenantId fields are fed from Azure Portal AD Application Blade , where I have created the App Registration. On
the detail page of this application is the Overview blade - which
gives reference to ClientId, TenantId.
ClientSecret has been created at the same page, at Certificates & secrets blade.
Domain is provided from Azure Domain list blade
User roles (Reader/Admin) have been created at the above (Azure AD Application) page.
Still on the same page:
Authentication Tab a new Web platform was added with Redirect URI https://localhost:7293/signin-oidc. As for Front-Channel logout https://localhost:44321/signout-oidc was provided.
ID Tokens is ticked under Implicit grant and hybrid flows section
Single Tenant option (only allow organization accounts)
Public flows disabled
In Enterprise Applications my application is listed. In the details view of the application:
Properties tab: Assignment required is set to false. I do not want to preassign users to the app
Visible to users: set to false
Users and groups tab: the Security groups have been created for which the Roles (Reader/Admin) are assigned to. Members are assigned to Security groups. For testing purposes I have added my Azure AD User as well with Reader role.
Single Sign-On: Here I see the following message: The single sign-on configuration is not available for this application in the Enterprise applications experience. {{my-application-name}} was created using the App registrations experience
Sign-in logs: oddly enough I see here my logins, so I assume that the role claims are not transferred to my WebApplication, since I get Unauthorized error upon opening the view at the controller.
The WebApp references
Config sections I have mapped to a model so I can inject if needed as IOptions
MicrosoftGraphConfiguration to map GraphApiUrl:
public class MicrosoftGraphConfiguration
{
public const string ConfigurationName = "MicrosoftGraph";
public string? GraphApiUrl { get; set; }
}
AzureAdConfiguration to map AzureAd configurations
public class AzureAdConfiguration
{
public const string ConfigurationName = "AzureAd";
public string? ClientId { get; set; }
public string? TenantId { get; set; }
public string? ClientSecret { get; set; }
public string? Domain { get; set; }
public string? Instance { get; set; }
public string? BaseUrl { get; set; }
public string[]? ClientCapabilities { get; set; }
public string[]? Scopes { get; set; }
public string? CallbackPath { get; set; }
}
Program.cs to add configure and add services to the DI container, etc.
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MicrosoftGraphConfiguration>(builder.Configuration.GetSection(MicrosoftGraphConfiguration.ConfigurationName));
builder.Services.Configure<AzureAdConfiguration>(builder.Configuration.GetSection(AzureAdConfiguration.ConfigurationName));
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "User.Read" })
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AssignmentToReaderRoleRequired", policy => policy.RequireRole("Reader"));
options.AddPolicy("AssignmentToAdminRoleRequired", policy => policy.RequireRole("Admin"));
});
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.RoleClaimType = "roles";
});
// Add services to the container.
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Controller actions for testing purposes:
[HttpGet]
public async Task<IActionResult> IndexAsync()
{
GetTenantDetailsResult tenantDetails = new();
if (this.tenantService != null)
{
tenantDetails = await this.tenantService.GetTenantDetailsAsync();
}
return View(new TenantViewModel(tenantDetails));
}
[HttpGet]
[Route("/Reader")]
[Authorize(Policy = AuthorizationPolicies.AssignmentToReaderRoleRequired)]
public IActionResult Reader()
{
var asd = User;
return Ok("He is a reader");
}
[HttpGet]
[Route("/Admin")]
[Authorize(Policy = AuthorizationPolicies.AssignmentToAdminRoleRequired)]
public IActionResult Admin()
{
return Ok("He is an admin");
}
Launching the app (without cookies - incognito) redirects me to
https://login.microsoft.com/{my-tenantId}/oauth2/authorize?.... Once logged in, endpoints without the [Authorize(...)] attribute works well.
Opening any of the above endpoints (where [Authorize] attribute is in place) gives me HTTP 404 and redirects to:
https://localhost:7293/MicrosoftIdentity/Account/AccessDenied?ReturnUrl=%2Freader
According to the docs, the Roles Claim should be sent together with any ID Tokens.
"These assigned app roles are included with any token that's issued for your application, either ... or ID tokens when your app is signing in a user"
I am unsure -thus my questions are- if:
The roles claim are not sent?
The roles claim are sent but cannot be parsed
How to proceed with the debugging to find out the issue
As a weird behavior.. if I open the application/Reader endpoint in Incognito/InPrivate browser window, I get a HTTP 500 response code after the login with the following error details:
MsalServiceException: 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: AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app
Even though the ClientSecret is as above mentioned, I believe it is correct.
A reason for the Http 500 might be that currently I do not maintain an endpoint for https://localhost:7293/signin-oidc
I appreciate helps, as I am a bit lost here for 3rd day.
Thx
I tried to reproduce the same in my environment and got below results:
I registered one Azure AD application named WebApp-RolesClaims and followed the same steps mentioned on GitHub
I have adjusted the appsettings.json file with same values as you like below:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "my-tenant-name",
"TenantId": "my-tenant-guid",
"ClientId": "my-client-guid",
"CallbackPath": "/signin-oidc"
"SignedOutCallbackPath ": "/signout-callback-oidc",
"Scopes": "user.read,profile,openid",
// To call an API
"ClientSecret": "my-client-secret-guid"
}
Now I ran the sample, and it took me to Microsoft login page.
When I signed in with Azure AD user credentials, I got consent screen like below:
After accepting the above consent, it gave me same exception as you like below:
As the error message says, it usually occurs if you include Client secret ID instead of Client secret value.
To resolve the error, you need replace "ClientSecret" with below:
Note that, key value will be visible only for few seconds after secret's creation and you cannot retrieve it later.
If that's the case, delete the existing secret and create new secret to copy it's value like below:
I have adjusted the appsettings.json file by replacing Client Secret value as below:
When I ran the sample now, I got the claims of signed in user including roles like below:
So, try changing Client Secret with key value in your appsettings.json file .
The browser app
I have a browser app (CRA, TypeScript) which is issuing, after successfully authenticating to Azure AD, a request to my API:
public async acquireAccessToken(): Promise<string | undefined> {
let res: AuthResponse | undefined = undefined;
const params: AuthenticationParameters = {
scopes: ["Users.Read"],
};
try {
res = await this.msal.acquireTokenSilent(params);
} catch (error) {
res = await this.msal.acquireTokenPopup(params);
}
return !res || !res.accessToken ? undefined : res.accessToken;
}
The one before is a utility method to get the access token to contact the API, the actual call is here:
const token = await acquireAccessToken();
const res = await fetch("/controller/test", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
});
console.log(res.text());
Where msal is the UserAgentApplication I am using as client to handle authentication and authorization in my browser app.
I have everything correctly set up in Azure where a registration app is used to represent the browser app, and another registration app is used to describe the API I need to contact.
The API
The API server is an ASP.NET Core 3.1 C# application whose Startup.cs is:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}
}
I have removed all the extra code and left the parts that concern auth.
The controller I am contacting is:
[ApiController]
[Route("controller")]
public class MyController : ControllerBase
{
[HttpGet("test/")]
[Authorize(Roles = "Admin")]
public async Task<string> Test()
{
return "Ok";
}
[HttpGet("test2/")]
[Authorize]
public async Task<string> Test2()
{
return "Ok";
}
[HttpGet("test3/")]
public async Task<string> Test3()
{
return "Ok";
}
}
Azure
The setup in Azure is simple: apart from the two app registrations for the browser app and the API, I have set in the browser app registration some custom roles and assigned them to some users.
I authenticate in the browser app using a user who has the Admin app role assigned to it.
The problem
When my client app tries to fetch data using these endpoints:
/controller/test3
/controller/test2
Everything is fine as one is unprotected and the other one uses a simple [Authorize].
However when trying to fetch from /controller/test, I get 403 (Forbidden).
Why can't I make the roles work?
More info
While debugging when fetching test2, I can see, in the controller, that this.User is present and there are several claims. Among those claims, I cannot see anything relating to the role. The access token I get has the following form:
{
"aud": "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"iss": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
"iat": 1607034050,
"nbf": 1607034050,
"exp": 1607037950,
"acr": "1",
"aio": "AWQAm/8RAAAAH1j5tZzINJFi5fsMsgf99gcrnqQA+dOhWBpFmsgy3jsr0pFJ0AxvenqthiNLmRqKzqx6l+9SuLlRniAVCTOoqEE7MonnOetO3h7g1/Bm520rS0qiX/gpCCWYm/UwDlJ+",
"amr": [
"pwd"
],
"appid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"appidacr": "0",
"email": "xxx#xxx.xxx",
"idp": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
"ipaddr": "xxx.xxx.xxx.xxx",
"name": "XXX",
"oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"rh": "0.AAAASupzDdEU8EyBI3R6nFeJQHVORvhJZ2hDjJoEO5yPUcZ0AEU.",
"scp": "Users.Read",
"sub": "frR45l2dTAIyXZ-3Yn2mGNbBcBX9CrGisgJ4L8zOCd4",
"tid": "0d73ea4a-14d1-4cf0-8123-747a9c578940",
"unique_name": "xxx#xxx.xxx",
"uti": "39dk-rAAP0KiJN5dwhs4AA",
"ver": "1.0"
}
As you can see, no claim relating to roles.
But note that I can successfully get the role in the user token I get when authenticating. I need that claim to flow in the access token too when I use it to contact the API. How?
I found your problem:
You cannot set a custom role in the manifest of the browser application. You need to set a custom role in the manifest of the api application, and then assign the role to the user.
Then you need to use the auth code flow to get the access token, and the roles claims will be included in it.
In fact, the access token is created based on the intended recipient of your token (ie your api). If you want to access the api, you must have permissions or roles. In your question, this role is you Custom, when you grant the user a custom role of the api application, then the user has the role that can access the api application. (Note that this is not to assign the custom role of the client application to the user, because you need to access the api application, so the role of api application is required), then the user can log in to the client application and request a token from the api application. When you obtain the token and use the token to access the API, the API only needs to verify whether the user you log in has the role to access it.
This was a hard nut to crack and is not available in auth0 by default.
You can set roles in the Auth0 id token for OpenId, But you have to write a rule in auth0 dashboard:
Go To Rules section on your Auth0 dashboard and create a new rule:
and then use this code to add the user role claims to the id token, that will be returned in the JWT token claims:
function addRolesToAccessToken(user, context, callback) {
const namespace = 'http://schemas.microsoft.com/ws/2008/06/identity/claims';
const assignedRoles = (context.authorization || {}).roles;
let idTokenClaims = context.idToken || {};
let accessTokenClaims = context.accessToken || {};
idTokenClaims[`${namespace}/role`] = assignedRoles;
accessTokenClaims[`${namespace}/role`] = assignedRoles;
context.idToken = idTokenClaims;
context.accessToken = accessTokenClaims;
return callback(null, user, context);
}
Hope this helps! Cheers
I've been looking for an answer on Internet for days with regards to [Authorize] over the SignalR Hub class. I'm using Azure B2C to authenticate users. Everything works great when the class is not decorated with [Authorize], however I require the user to be authorized, so that I can access the Claims. All my Controllers are authenticating correctly.
[Authorize]
public class SignalRHub : Hub
{
My SignalR Service is running on Azure and started on the server as follows:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
.....
services.AddSignalR().AddAzureSignalR(ConnectionString)
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoint =>
{
.....
endpoint.MapHub<AzureSignalRSevice.SignalRHub>("/rhub");
});
}
The Debugger is indicating when the client tries to connect:
Microsoft.AspNetCore.Hosting.Diagnostics: Information: Request starting HTTP/1.1 POST https://localhost:44301/rhub/negotiate?negotiateVersion=1 0
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AzureADB2CJwtBearer was not authenticated. Failure message: No SecurityTokenValidator available for token: {Token}
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: AzureADB2CJwtBearer was challenged.
The client code is as follows:
var connection = new HubConnectionBuilder().WithUrl("https://localhost:44301/rhub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
}).Build();
All the articles I have read say that the token is passed as a parameter, however in my instance it is being sent in the Authorization header correctly.
I have tried to configure the JwtBearerOptions and pass the token to context.Token, however I get the same Authentication failure.
services.Configure<JwtBearerOptions>(AzureADB2CDefaults.JwtBearerAuthenticationScheme, options =>
{
}
OnChallenge is hit when it fails with invalid_token in the context.
All the Packages are the most recent and up to date running on Core 3.1.2
I've been though many articles, this was the best so far
https://github.com/dotnet/aspnetcore/issues/10582
It doesn't use B2C Authetication though.
I have it working !
The solution is to include the Authentication Scheme
[Authorize(AuthenticationSchemes = AzureADB2CDefaults.BearerAuthenticationScheme + ", " + AzureADB2CDefaults.JwtBearerAuthenticationScheme)]
public class SignalRHub : Hub
{
}
we got an application deployed as App Service and we are using SignalR for communication. After enabling AAD authentication - in browsers we started receiving 302 responses with redirect location to Azure AD.
Seems like the authentication layer on App Service is ignoring access_token passed by query string.
Request
Request URL: wss://<url>/hubs/chat?access_token=<token>
Request Method: GET
Response
Status Code: 302 Redirect
Location: https://login.windows.net/common/oauth2/authorize?...
After looking everywhere we couldn't find any solution to make this work.
The only solution to this issue that we see is either to disable authentication on App Service or use Long-Pooling, but both options are not acceptable in our situation.
By default, you web application will not get the access token from query string. Commonly, it will get the access token from authorization header or the cookie.
To get the access token from query string, you need to implement your custom authentication way.
Install Microsoft.Owin.Security.ActiveDirectory NuGet package.
Create an authentication provider which will get access token from query string.
public class QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get("access_token");
if (!string.IsNullOrEmpty(value))
{
context.Token = value;
}
return Task.FromResult<object>(null);
}
}
Add map in .
app.Map("/yourpath", map =>
{
map.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Provider = new QueryStringOAuthBearerProvider(),
Tenant = tenantId,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = clientId
}
});
map.RunSignalR(hubConfiguration);
});
After multiple calls with Microsoft Technical Support, MS confirmed that App Service Authentication layer doesn't support access token passed in query string and there are no plans for this support yet. So there are two options:
Use different protocol for SignalR (long pooling works just fine)
Drop App Service Authentication
Using a custom middleware, I was able to update the request prior to authorization occurring:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace Stackoverflow.Example.Security.Middleware
{
public class BearerTokenFromQueryToHeaderMiddleware
{
private readonly RequestDelegate _next;
public BearerTokenFromQueryToHeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Query["access_token"];
if (!string.IsNullOrWhiteSpace(token))
{
context.Request.Headers.Add("Authorization", $"Bearer {token}");
}
await _next(context);
}
}
}
I didn't try to get this working with the OpenID framework, but I did test using a custom policy. As long as this is registered earlier than the authentication, then this middleware should execute prior to the framework looking for the token in the header.
I followed official steps as below to try the scenario "web app calling a Web API in Azure Ad B2C", the only difference is I am using Asp.Net core. I am using AuthorizationCode to get the access token, but it always returns with id token and NULL access token.
Create an Azure AD B2C tenant.
Register a web api.
Register a web app.
Set up policies.
Grant the web app permissions to use the web api.
My code:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
AutomaticChallenge = true,
ClientId = aadB2cSettings.ClientId,
MetadataAddress = $"{aadB2cSettings.Instance}{aadB2cSettings.Tenant}/v2.0/.well-known/openid-configuration?p={aadB2cSettings.B2cSignUpOrSignInPolicy}",
PostLogoutRedirectUri = aadB2cSettings.RedirectUrl,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async context =>
{
var authCode = context.TokenEndpointRequest.Code;
var b2cAuthority = $"{aadB2cSettings.Instance}tfp/{aadB2cSettings.Tenant}/{aadB2cSettings.B2cSignUpOrSignInPolicy}/v2.0/.well-known/openid-configuration";
var cca = new ConfidentialClientApplication(
aadB2cSettings.ClientId,
b2cAuthority,
aadB2cSettings.RedirectUrl,
new ClientCredential(aadB2cSettings.ClientSecret),
new TokenCache(),
null);
try
{
var authResult = await cca.AcquireTokenByAuthorizationCodeAsync(authCode, new[] { "https://hulab2c.onmicrosoft.com/b2cdemo/all" });
context.HandleCodeRedemption(authResult.AccessToken, authResult.IdToken);
}
catch (Exception ex)
{
throw ex;
}
}
},
Used fiddler to capture the request, it is:
POST
https://login.microsoftonline.com/hulab2c.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_signuporsignin
HTTP/1.1
Request Body:
client_id=1ff91f47-08ee-4973-83f4-379ad7e0679c&client_info=1&client_secret=......&scope=https%3A%2F%2Fhulab2c.onmicrosoft.com%2Fb2cdemo%2Fall+offline_access+openid+profile&grant_type=authorization_code&code=......&redirect_uri=https%3A%2F%2Flocalhost%3A44383%2F
Return:
{"id_token":"......","token_type":"Bearer","not_before":1494494423,"client_info":"......","scope":""}
So only id token, no access token. But we should get access token here, right?
Finally found out my failure reason: the request to get AuthorizationCode doesn't contain the target scope. Reflect in code, for OpenIdConnectOption in aspnetcore, the Scope parameter is readonly and its default value is "opened profile".
Scope is readonly in OpenIdConnectOption
So the default authorization code request sent is:
GET
https://login.microsoftonline.com/hulab2c.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_signuporsignin&client_id=7f865ca0-271e-4f27-be21-6f0072fe3ad7&redirect_uri=https%3A%2F%2Flocalhost%3A44355%2Fsignin-oidc&response_type=code%20id_token&scope=openid%20profile&response_mode=form_post&nonce=......
HTTP/1.1
Thus, using this authorization code in response to get token, even we set right scope in the token request, we still can't get the access code but only id token, because the provide authorization code is only for "openid profile".
To fix this, we need to add target web api scope into the authorization code as well. Here is the how-to-fix code:
Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.Scope += $" offline_access {myapiscope}";
return Task.FromResult(0);
},
......
}
In AspNet, we don't need to do this because its scope is not readonly as aspnetcore and can be set directly:
new OpenIdConnectAuthenticationOptions
{
......
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}"
}
https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/issues/4 Microsoft have reproduced the issue and working on fix