I have a REST API (core 2.1) which need to support a SPA, providing restful endpoints and some live-interactive features using SignalR;
The Hub/MVC routes are running on the same server, which is also the provider of the JWT token.
After loggin in, the client-side receives a JWT token, which places on the header for every REST request, otherwise it gets a 401 (This is working with the [Authorize] attribute).
At the client-side, the code below tries to connect to my /hub endpoint:
new HubConnectionBuilder().withUrl(HUB_URL, { accessTokenFactory: () => this.getToken() })
And if I place [Authorize] at my Hub class, I get the following error (Without the authorization, the client can send and listen correctly):
WebSocket connection to 'wss://localhost:5001/hub?id=MY_ID&access_token=MY_TOKEN' failed: HTTP Authentication failed; no valid credentials available
The server logged failed authentications:
(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/hub?
id=MY_ID&access_token=MY_TOKEN
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.3658ms 401
Differently from the those requests, using the SAME JWT TOKEN with the REST ones (Using [Authorize]), with the Header: Bearer XXXX instead of querystring, triggers the OnTokenValidated. The OnAuthenticationFailed is never triggered, even if the authentication fails:
(Triggered a console.log on AddJwtBearerOptions.Events.OnMessageReceived)
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
Request starting HTTP/1.1 GET https://localhost:5001/api/products application/json
(Triggered a console.log on AddJwtBearerOptions.Events.OnTokenValidated)
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[2]
Successfully validated the token.
Check below my ´Startup.cs`
ConfigureServices(IServiceCollection)
services.AddSignalR();
services.AddCors(option => option.AddPolicy("CorsPolicy", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().AllowCredentials()));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters{
ValidateIssuer = true,
ValidIssuer = Configuration["JWT:Issuer"],
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecurityKey"]))
};
});
Configure(IApplicationBuilder)
app.UseAuthentication();
app.UseCors("CorsPolicy");
app.UseHttpsRedirection();
app.UseMvc();
app.UseSignalR(routes => {
routes.MapHub<ApplicationHub>("/hub");
});
You also need to add this block inside the .AddJwtBearer section:
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hub")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
This can be found here in the docs.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.3658ms 401
Spend 20 hours for fixing. :(
Reason was in:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class ChatHub : Hub<IChatClient>
My service configuration:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => ...)
UPD:
Full work example with JS client here.
To expand on what #marcusturewicz already have stated, one should also check for the token inside the Authorization header in case if the access token is sent in the header instead of query.
Although, this may vary from how e.g., the client implementer sends the token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// If no token is present in the query, we can
// try to get it from the Authorization header.
StringValues accessToken = context.Request.Query["access_token"] == StringValues.Empty
? context.Request.Headers["Authorization"]
: context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hub")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
Related
I am using Blazor WebAssembly Asp.Net Core hosted PWAand integrated the AspNetCore.Identity into it. I created the AuthenticationStateProvider in the Client-Side and now I want to allow the user access to a controller where he needs to be authorized.
I have tested via postman, the users were been created and stored in DB as aspnetusers with the right credentials. The Login/Account Controller work as I wanted it.
When the user is authorized it tells this exception in the browser when accessing the authorized controller request:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Response status code does not indicate success: 401 (Unauthorized).
System.Net.Http.HttpRequestException: Response status code does not
indicate success: 401 (Unauthorized).
Startup.cs (ConfigureServices-Method):
...
serviceCollection.AddDbContext<SQLiteTestDbContext>(options =>
{
options.UseSqlite(config["ConnectionStrings:SQLiteTestConnection"]);
});
serviceCollection.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<SQLiteTestDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
services.AddHttpContextAccessor();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Program.cs Client-Side
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Logging.SetMinimumLevel(LogLevel.Warning);
//Registering Shared-Library models
builder.Services.AddScoped<ObjectModel>();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
builder.Services.AddScoped<IAuthService, AuthService>();
//Registered BlazorContextMenu Service
builder.Services.AddBlazorContextMenu();
//Registering FileReader service, for image upload -> Azure
builder.Services.AddFileReaderService(options => options.UseWasmSharedBuffer = true);
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
My Controller with authorize attribute:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class ObjectController : ControllerBase
{
....
Note:
When your user tries to access a protected (annotated with the Authorize attribute) page on the client he should login first or register.
In order to register, he should be redirected to an Account Controller where you should create a new user, and add it to the database (You said you " integrated the AspNetCore.Identity into it"), which is fine...and should be used to authenticate and verify the user's identity. You account controller should also produce a Jwt Token that should be passed to the client app, and stored in the local storage.
Now, whenever your user tries to access protected resources on your Web Api endpoints, you should retrieve the Jwt Token from the local storage, and add it to the request header. If you do so, the Unauthorized response would be something of the past.
Custom AuthenticationStateProvider can be a good place from which you can manage storing the Jwt Token in the local storage and retrieving it for outbound HTTP request calls.
Here's some sample code to clarify what you should do:
#code {
WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
var token = await TokenProvider.GetTokenAsync();
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
"api/WeatherForecast",
new AuthenticationHeaderValue("Bearer", token));
}
}
Note: TokenProvider is a custom AuthenticationStateProvider that defines a method called GetTokenAsync that provides (reading the Jwt Token from the local storage and passing it to the calling code) the Jwt Token
Hope this helps...
In case of Linux App Service in combination with ID Server the Authority needs to be set according to Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#azure-app-service-on-linux-1
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = "{AUTHORITY}";
});
Example: options.Authority = "https://contoso-service.azurewebsites.net";
When using IdentityServer3.AccessTokenValidation in my .Net 4.8 (not core) WebAPI to secure the endpoints, the WebAPI always returns a 401 Not Authorized to the client app. The token is supplied by IDS4. When the client is calling my Asp.Net Core API, Authorization is successful. But when calling the .Net 4.8 AspNet WebApi, it is not.
Client MVC App Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "TestMvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Add("testApi");
options.CallbackPath = "/auth";
options.SignedOutRedirectUri = "~/home";
});
}
The .Net Core API Startup that works:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "testApi";
options.RequireHttpsMetadata = false;
options.RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Startup.cs of the .Net 4.8 WebApi that always returns 401:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.EnableSystemDiagnosticsTracing();
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
var idsOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
RequiredScopes = new string[] { "testApi" },
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role
};
app.UseIdentityServerBearerTokenAuthentication(idsOptions);
app.UseWebApi(config);
}
The Asp.Net Core Controller Endpoint that executes successfully
[Route("test")]
[Authorize(Roles = "[A role claim that is definitely present in access token]")]
public class TestController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Success");
}
}
The Asp.Net 4.8 Controller Endpoint that returns 401
[Route("test")]
[Authorize(Roles = "[A role claim that is definitely present in access token]")]
public class TestController : ApiController
{
[HttpGet]
public IHttpActionResult Get()
{
return Ok("Success");
}
}
MVC Client App Calling API:
public async Task<IActionResult> AccessApi()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("http://localhost:5001/test");
ViewBag.Json = content;
return View("json");
}
The last lines of the IDS server log:
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Request path /.well-known/openid-configuration matched to endpoint type Discovery dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryEndpoint info:
IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for
/.well-known/openid-configuration dbug:
IdentityServer4.Endpoints.DiscoveryEndpoint[0]
Start discovery request info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 17.3987ms 200 application/json; charset=UTF-8 info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration/jwks dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Request path /.well-known/openid-configuration/jwks matched to endpoint type Discovery dbug:
IdentityServer4.Hosting.EndpointRouter[0]
Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryKeyEndpoint info:
IdentityServer4.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryKeyEndpoint for
/.well-known/openid-configuration/jwks dbug:
IdentityServer4.Endpoints.DiscoveryKeyEndpoint[0]
Start key discovery request info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 16.6027ms 200 application/jwk-set+json; charset=UTF-8
The token is issued successfully and passed in the request headers to the API.
The API seems to be calling the discover endpoint correctly, but then nothing after that. I would expect the API to validate the incoming token with the IDS server, but there is no call to the IDS server for validation. Confirmed with both the IDS log and using Fiddler to check the traffic.
This can be caused by couple of things, to be sure we need the log of ASP.NET API project. Here is some suggested fixes for similar issues:
On Statup.cs class of IdentityServer project
Change AccessTokenJwtType to JWT, default value on IdentityServer4 is at+jwt but .Net Framework Api (OWIN/Katana) requires JWT.
Add /resources aud by setting EmitLegacyResourceAudienceClaim to true, this is removed on IdentityServer4.
You can verify the access_token on https://jwt.ms/ by checking "typ" and "aud" .
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
options.EmitLegacyResourceAudienceClaim = true;
});
On Statup.cs class of .Net Framework Api project, set ValidationMode to ValidationMode.Local, access token validation endpoint used by this method is removed on IdentityServer4.
var idsOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
RequiredScopes = new string[] { "testApi" },
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
ValidationMode = ValidationMode.Local,
};
I have sample working implementation here, and also a blog post contains more details https://nahidfa.com/posts/identityserver4-and-asp-.net-web-api/
I strongly suggest you to gather logs on API, this helps to find the actual issue in your case and finding the fix. here is a sample to turn on OWIN log on Api.
The API client is using either JWT token issued by API itself (standard) or by Azure AD.
When I enable ONLY the custom (standard) bearer authentication, everything works perfectly, without any issues.
Also, when I enable ONLY the Azure AD bearer authentication, everything works perfectly, also.
When I enable both of them, one of them stops working.
Here is my setup of the .Net core API:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(BuildStandardJwtBearerOptions);
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
private void BuildStandardJwtBearerOptions(JwtBearerOptions options)
{
var settings = GetStandardTokenSettings(null);
options.IncludeErrorDetails = true;
options.RequireHttpsMetadata = false;
options.SaveToken = true;
var signingKeyBytes = Encoding.UTF8.GetBytes(settings.SecretKey);
var signingKey = new SymmetricSecurityKey(signingKeyBytes);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = settings.Issuer,
ValidAudience = settings.Issuer,
IssuerSigningKey = signingKey
};
}
Here is an example error for when client is sending Azure AD token:
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information:
Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException:
IDX10500: Signature validation failed. No security keys were provided
to validate the signature. at
System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String
token, TokenValidationParameters validationParameters) at
System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String
token, TokenValidationParameters validationParameters, SecurityToken&
validatedToken) at
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information:
AzureADJwtBearer was not authenticated. Failure message: IDX10500:
Signature validation failed. No security keys were provided to
validate the signature.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information:
Route matched with {action = "List", controller = "Account"}.
Executing action BookRental.Api.Controllers.AccountController.List
(BookRental.Api)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information:
Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information:
Authorization failed for the request at filter
'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing
ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information:
AuthenticationScheme: AzureADJwtBearer was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information:
Executed action BookRental.Api.Controllers.AccountController.List
(BookRental.Api) in 7.1108ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request
finished in 16.8394ms 401
How can I make those two types of tokens to work side by side?
You can try to change the default policy of the authorization system by indicating both the authentication schemes :
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options))
.AddJwtBearer("scc", BuildStandardJwtBearerOptions);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(AzureADDefaults.BearerAuthenticationScheme, "scc")
.Build();
});
I have the stock file | new | web | asp.net core web api project template where I selected AzureAD authentication that generated the following Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
and the following appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "mymsdn.onmicrosoft.com",
"TenantId": "<my azuread tenant id>",
"ClientId": "<my azuread web app id>"
}
I'm using postman to acquire token leveraging a public client profile app setup just as I have done for another web api setup that is working as expected with the same azureAd bearer token auth code and settings coordinates.
For some reason this app is trying to validate the wrong token issuer format and i'm at a loss as to how I correct it.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AzureADJwtBearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'https://login.microsoftonline.com/<my azuread tenantid>/v2.0'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://sts.windows.net/<my azuread tenantid>/'.
Turns out this issue surfaces if you configured azuread app registrations entry for your ClientId to support any organization or consumer, aka microsoft account, signins instead of just your organization or any organization. The fix was to use the AddJwtBearer() block of Startup.ConfigureServices() code shown below instead of the project template provided AddAzureADBearer() block.
public void ConfigureServices(IServiceCollection services)
{
// if azuread app registrations entry for ClientId has "signInAudience": "AzureADMyOrg" or "AzureADMultipleOrgs" where "iss": "https://sts.windows.net/{TenantId}/"
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => //Configuration.Bind("AzureAd", options));
{
Configuration.Bind("AzureAd", options);
Log.LogInformation($"the AddAzureADBearer options have been configured for ClientId = {options.ClientId}");
});
// if azuread app registrations entry for ClientId has "signInAudience": "AzureADandPersonalMicrosoftAccount" where "iss": "https://login.microsoftonline.com/{TenantId}/v2.0"
services.AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; })
.AddJwtBearer(options =>
{
var azureadoptions = new AzureADOptions(); Configuration.Bind("AzureAd", azureadoptions);
options.Authority = $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2";
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = $"{azureadoptions.ClientId}",
//ValidAudiences = new List<string> { $"{azureadoptions.ClientId}", $"api://{azureadoptions.ClientId}", $"https://myapp.azurewebsites.net/" },
//ValidIssuer = $"https://sts.windows.net/{azureadoptions.TenantId}/" // for "signInAudience": "AzureADMyOrg" or "AzureADMultipleOrgs"
ValidIssuer = $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2.0" // for "signInAudience": "AzureADandPersonalMicrosoftAccount"
//ValidIssuers = new List<string> { $"https://sts.windows.net/{azureadoptions.TenantId}/", $"{azureadoptions.Instance}{azureadoptions.TenantId}/v2.0" }
};
Log.LogInformation($"the AddJwtBearer options have been configured for ClientId = {azureadoptions.ClientId}");
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
I was trying to authenticate existing token from uwp-app in web-api and I had a similar issue:
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'https://spolujizda.b2clogin.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://login.microsoftonline.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'.
at Microsoft.IdentityModel.Tokens.Validators.ValidateIssuer(String issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateIssuer(String issuer, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AzureADB2CJwtBearer was not authenticated. Failure message: IDX10205: Issuer validation failed. Issuer: 'https://spolujizda.b2clogin.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'. Did not match: validationParameters.ValidIssuer: 'null' or validationParameters.ValidIssuers: 'https://login.microsoftonline.com/4e8094c9-5058-454c-b201-ef61d7ae6619/v2.0/'.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Get", controller = "Values"}. Executing action Spolujizda.ApiServer.Controllers.ValuesController.Get (Spolujizda.ApiServer)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler:Information: AuthenticationScheme: AzureADB2CJwtBearer was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action Spolujizda.ApiServer.Controllers.ValuesController.Get (Spolujizda.ApiServer) in 8.105ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 8950.4739ms 401 text/plain
But for me the fix was much simpler.
For Web API I had AzureAdB2C.Instance set to https://spolujizda.b2clogin.com/tfp/.
For an UWP app, I had been issuing token via https://login.microsoftonline.com/tfp/
That is why it resulted in this error. Because in the UWP app, the token was issued for login.microsoft... and in the Web API, it was trying to verify issuer as spolujizda.b2clogin...
First I tried to change address for issuing token for uwp-app, but that didn't work.
So secondly I just changed web-api's AzureAdB2C.Instance config to login.microsoft.... and it now works.
I have two aspnet.core services. One for IdentityServer 4, and one for the API used by Angular4+ clients. The SignalR hub runs on the API. The whole solution runs on docker but that should not matter (see below).
I use implicit auth flow which works flawlessly. The NG app redirects to the login page of IdentityServer where the user logs in. After that the browser is redirected back to the NG app with the access token. The token is then used to call the API and to build up the communication with SignalR. I think I've read everything that is available (see sources below).
Since SignalR is using websockets that does not support headers, the token should be sent in the querystring. Then on the API side the token is extracted and set for the request just as it was in the header. Then the token is validated and the user is authorized.
The API works without any problem the users gets authorized and the claims can be retrieved on the API side. So there should be no problem with the IdentityServer then since SignalR does not need any special configuration. Am I right?
When I do not use the [Authorized] attribute on the SignalR hub the handshake succeeds. This is why I think there is nothing wrong with the docker infrastructure and reverse proxy I use (the proxy is set to enable websockets).
So, without authorization SignalR works. With authorization the NG client gets the following response during handshake:
Failed to load resource: the server responded with a status of 401
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
The request is
Request URL: https://publicapi.localhost/context/negotiate?signalr_token=eyJhbGciOiJSUz... (token is truncated for simplicity)
Request Method: POST
Status Code: 401
Remote Address: 127.0.0.1:443
Referrer Policy: no-referrer-when-downgrade
The response I get:
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:4200
content-length: 0
date: Fri, 01 Jun 2018 09:00:41 GMT
server: nginx/1.13.10
status: 401
vary: Origin
www-authenticate: Bearer
According to the logs, the token is validated successfully. I can include the full logs however I suspect where the problem is. So I will include that part here:
[09:00:41:0561 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
[09:00:41:0564 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
I get these in the log file and I am not sure what it means. I include the code part on the API where I get and extract the token along with the authentication configuration.
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.Authority = "http://identitysrv";
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
options.JwtBearerEvents.OnMessageReceived = context =>
{
if (context.Request.Query.TryGetValue("signalr_token", out StringValues token))
{
context.Options.Authority = "http://identitysrv";
context.Options.Audience = "publicAPI";
context.Token = token;
context.Options.Validate();
}
return Task.CompletedTask;
};
});
There are no other errors, exceptions in the system. I can debug the app and everything seems to be fine.
What does the included log lines mean?
How can I debug what is going on during the authorization?
EDIT: I almost forgot to mention, that I thought the problem was with the authentication schemes so, I set every scheme to the one I think was needed. However sadly it did not help.
I am kind of clueless here, so I appreciate any suggestion. Thanks.
Sources of information:
Pass auth token to SignalR
Securing SignalR with IdentityServer
Microsoft docs on SignalR authorization
Another GitHub question
Authenticate against SignalR
Identity.Application was not authenticated
I have to answer my own question because I had a deadline and surprisingly I managed to solve this one. So I write it down hoping it is going to help someone in the future.
First I needed to have some understanding what was happening, so I replaced the whole authorization mechanism to my own. I could do it by this code. It is not required for the solution, however if anyone needed it, this is the way to do.
services.Configure<AuthenticationOptions>(options =>
{
var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
scheme.HandlerType = typeof(CustomAuthenticationHandler);
});
With the help of IdentityServerAuthenticationHandler and overriding every possible method I finally understood that the OnMessageRecieved event is executed after the token is checked. So if there weren't any token during the call for HandleAuthenticateAsync the response would be 401. This helped me to figure out where to put my custom code.
I needed to implement my own "protocol" during token retrieval. So I replaced that mechanism.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority = "http://identitysrv";
options.TokenRetriever = CustomTokenRetriever.FromHeaderAndQueryString;
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
});
The important part is the TokenRetriever property and what comes to replace it.
public class CustomTokenRetriever
{
internal const string TokenItemsKey = "idsrv4:tokenvalidation:token";
// custom token key change it to the one you use for sending the access_token to the server
// during websocket handshake
internal const string SignalRTokenKey = "signalr_token";
static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }
static CustomTokenRetriever()
{
AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
}
public static string FromHeaderAndQueryString(HttpRequest request)
{
var token = AuthHeaderTokenRetriever(request);
if (string.IsNullOrEmpty(token))
{
token = QueryStringTokenRetriever(request);
}
if (string.IsNullOrEmpty(token))
{
token = request.HttpContext.Items[TokenItemsKey] as string;
}
if (string.IsNullOrEmpty(token) && request.Query.TryGetValue(SignalRTokenKey, out StringValues extract))
{
token = extract.ToString();
}
return token;
}
And this is my custom token retriever algorithm that tries the standard header and query string first to support the common situations such as web API calls. But if the token is still empty it tries to get it from the query string where client put it during websocket handshake.
EDIT: I use the following client side (TypeScript) code in order to provide the token for the SignalR handshake
import { HubConnection, HubConnectionBuilder, HubConnectionState } from '#aspnet/signalr';
// ...
const url = `${apiUrl}/${hubPath}?signalr_token=${accessToken}`;
const hubConnection = new HubConnectionBuilder().withUrl(url).build();
await hubConnection.start();
Where apiUrl, hubPath and accessToken are the required parameters of the connection.
I know this is an old thread, but in case someone stumbles upon this like I did. I found an alternative solution.
TLDR: JwtBearerEvents.OnMessageReceived, will catch the token before it is checked when used as illustrated below:
public void ConfigureServices(IServiceCollection services)
{
// Code removed for brevity
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://myauthority.io";
options.ApiName = "MyApi";
options.JwtBearerEvents = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/myhubname")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
This Microsoft doc gave me a hint:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1. However, in the Microsoft example, options.Events is called, because it is not an example using IdentityServerAuthentication. If options.JwtBearerEvents is used the same way as options.Events in the Microsoft example, IdentityServer4 is happy!
Let me put my two cents to this. I think most of us store tokens in cookies and during WebSockets handshake they are also sent to the server, so I suggest using token retrieval from cookie.
To do this add this below last if statement:
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
token = cookieToken;
}
Actually we could delete retrieval from query string at all as according to Microsoft docs this is not truly secure and can be logged somewhere.