HubConnection Authentication Using Identity From Windows Service - c#

I have a Hub that serves up near real-time data. I want the provider of the real-time data to be able to send messages to all connected clients. The problem I am running into is authenticating the connection because the provider is a Windows Service. So, I am creating a client using HubConnectionBuilder. When I attempt to connect, I am getting a 401 authentication failure which is expected.
I am trying to figure out how to use the existing Identity authentication that exists in the shared Entity Framework database. Is there a way to get an authentication token using UserManager or SigninManager from this stand-alone service? Or is there a better approach to allow the HubConnection to authenticate?
Here is the connection code I am using:
var notifyConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/notify", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.WithAutomaticReconnect()
.Build();
notifyConnection.Closed += async (error) =>
{
if (Debug) _logger.LogInformation("Hub connection closed: " + error.Message);
await Task.Delay(new Random().Next(0, 5) * 1000);
await notifyConnection.StartAsync();
};
try
{
var task = notifyConnection.StartAsync();
task.Wait();
}
catch (Exception ex)
{
if (Debug) _logger.LogInformation("Hub connection start error: " + ex.Message);
}
_myAccessToken is not defined. I am trying to figure out how to get a valid access token that will authenticate properly.
EDIT:
I have tried adding JWT authentication. I can verify that the bearer token is getting generated properly. But it doesn't appear to be validated on the Hub.
I have added the following to the services configuration:
var key = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(Configuration["JwtKey"]));
services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, parameters) =>
expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = key,
NameClaimType = ClaimTypes.NameIdentifier
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
I have also updated the HubConnectionBuilder to use:
notifyConnection = new HubConnectionBuilder()
.WithUrl(baseUrl + "/notify", options =>
{
options.AccessTokenProvider = async () =>
{
var stringData = JsonConvert.SerializeObject(new { username = "****", password = "****" });
var content = new StringContent(stringData);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await httpClient.PostAsync(baseUrl + "/api/token", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
};
})
.WithAutomaticReconnect()
.Build();
It still returns a 401. When I tried to force the Authorize attribute to:
[Authorize(AuthenticationSchemes = "Bearer,Cookies")]
It then returns a 500 error.

So, after changing the Authorize attribute on the Hub class, I was able to get JWT authentication working along with the built-in Identity authentication.
Here is the new Authorize attribute:
[Authorize(AuthenticationSchemes = "Bearer,Identity.Application")]
I added a simple token generation API controller to feed the HubConnection. The following sources were very helpful:
https://www.codemag.com/article/1807061/Build-Real-time-Applications-with-ASP.NET-Core-SignalR
.Net SignalR use JWT Bearer Authentication when Cookie Authentication is also configured

Related

Blazor WASM: Add JWT auth with custom API

I have built an API that controls some smart home stuff. To prevent the whole internet from doing so, I added authentication using JWT / Bearer. The API contains endpoints for the smart home stuff aswell as some user management:
API endpoints for users
The login will return a JWT token if credentials were valid. It is also built using .NET 6:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
};
});
Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLogin login)
{
var user = await _userService.GetUser(login.Username);
if (user is not null && _userService.IsPasswordCorrect(user, login.Password))
{
var tokens = await _userService.GetJwtAndRefreshToken(user);
return Ok(new LoginResponse { JWT = tokens.Jwt, RefreshToken = tokens.Refreshtoken });
}
return Unauthorized("Wrong username or password!");
}
Now I am trying to build a frontend for this app using blazor. When creating the app, i used the option "individual user accounts" for authentication. It is documented here: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-6.0&tabs=visual-studio
This created the following in the blazow WASM app:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddOidcAuthentication(options =>
{
// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
builder.Configuration.Bind("Local", options.ProviderOptions);
});
await builder.Build().RunAsync();
appsettings.json looks like this:
{
"Local": {
"Authority": "https://localhost:7110/login",
"ClientId": "33333333-3333-3333-33333333333333333"
}
}
I changed the Authority to my login api url, but doesn't seem to be enough.
Clicking on the login button that was added by default fires this request:
Request
Is there a simple way to use the MS Authorization framework with my custom api?
I spent great amount of time on this. These are my notes from it. Note that I am using IdentityServer. Probably a lot of stuff will be different for you. But it should at least guide you what to check.
It works (for me), but best-practise is not garantee.
My API address is on port 5001, Client is on port 5101
For Client project
Change HttpClient address in Client. Change Http MessageHandler. Change address for public client
var clientBaseAddress = new Uri(builder.Configuration["apiurl"] ?? throw new ArgumentNullException("apirul is null (reading from config file)"));
builder.Services.AddHttpClient("BlazorApp6.ServerAPI", client =>client.BaseAddress = clientBaseAddress)
.AddHttpMessageHandler(sp =>
{//this is need when api is separated. https://code-maze.com/using-access-token-with-blazor-webassembly-httpclient/
var handler = sp.GetService<AuthorizationMessageHandler>()!
.ConfigureHandler(
authorizedUrls: new[] { builder.Configuration["HttpMessageHandlerAuthorizedUrls"] },
scopes: new[] { "BlazorApp6.ServerAPI" }
);
return handler;
});
builder.Services.AddHttpClient<PublicClient>(client => client.BaseAddress = clientBaseAddress);
Add HttpMessageHandlerAuthorizedUrls apiurl to appsettings (example for developement):
"apiurl": "https://localhost:5001",
"HttpMessageHandlerAuthorizedUrls": "https://localhost:5001",
Program.cs AddApiAuthorization is different (set opt.ProviderOptions.ConfigurationEndpoint)
builder.Services.AddApiAuthorization(
//this line is only when address of api consumer is different
opt => opt.ProviderOptions.ConfigurationEndpoint = builder.Configuration["ApiAuthorizationConfigurationEndpoint"]
).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
Add ApiAuthorizationConfigurationEndpoint to appsettings
"ApiAuthorizationConfigurationEndpoint": "https://localhost:5001/_configuration/BlazorApp6.Client"
Change launchSetting to different port
"applicationUrl": "https://localhost:5101;http://localhost:5100",
For api project
Add cors to client app
string developmentCorsPolicy = "dev_cors";
services.AddCors(opt =>
{
opt.AddPolicy(name: developmentCorsPolicy, builder =>
{
builder.WithOrigins("https://localhost:5101", "https://localhost:5201")
.WithMethods("GET", "POST", "PUT", "DELETE")
.AllowAnyHeader();
});
});
//...
if (app.Environment.IsDevelopment())
app.UseCors(developmentCorsPolicy);
There is probably some need to add cors for identiy server, but it works without it.
in case it is needed:
services.AddSingleton<ICorsPolicyService>((container) =>
{
var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
return new DefaultCorsPolicyService(logger)
{
AllowAll = true
};
});
Change appsettings IdentityServer section to have some info about client.
This info is obtained in OidcController with requests starting _configuration:
"IdentityServer": {
"Clients": {
"BlazorApp6.Client": {
"Profile": "SPA",
"LogoutUri": "https://localhost:5101/authentication/logout-callback",
"RedirectUri": "https://localhost:5101/authentication/login-callback"
},
},
"Key": {
"Type": "Development"
} }
Note that Profile has changed to SPA (instead of IdentityServerSPA, which means hosted)

AspNet.Security.Oauth.Spotify HttpContext.GetTokenAsync("spotify", "access_token") Returning a null token after second login+

When using the ASP.Net core 3.0 angular SPA individual account template, and the AspNet.Security.OAuth.Spotify nuget package. When A user logs in, I want to be able to get their spotify access token so I can preform actions on the user's behalf. However, when I call await HttpContext.GetTokenAsync("spotify", "access_token"); the results returns null.
I've debugged a bit and saw the spotify tokens on second+ login in the options.Events.OnCreatingTicket event, but I guess the token is just not passed around past that? I'm not really sure anymore.
Startup.cs
ConfigureServices
services.AddAuthentication()
.AddIdentityServerJwt()
.AddSpotify("spotify", options =>
{
options.ClientId = Configuration["SpotifySettings:ClientId"];
options.ClientSecret = Configuration["SpotifySettings:ClientSecret"];
options.CallbackPath = "/callback";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SaveTokens = true;
String[] items = {
"playlist-read-private", "playlist-modify-public", "playlist-modify-private", "playlist-read-collaborative", "user-library-modify", "user-library-read", "user-read-email"
};
foreach (var item in items)
{
options.Scope.Add(item);
}
options.Events.OnRemoteFailure = (context) =>
{
// Handle failed login attempts here
return Task.CompletedTask;
};
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
ctx.Properties.StoreTokens(tokens);
return Task.CompletedTask;
};
});
Code to try to retrieve token
if (!User.Identity.IsAuthenticated)
{
return StatusCode(403);
}
var client = httpClientFactory.CreateClient("spotify");
String spotifyToken = await HttpContext.GetTokenAsync("spotify", "access_token");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", spotifyToken);
var result = await client.GetAsync("v1/me/playlists");
I expected to get a spotify access_token that I can use to call the spotify api but spotifyToken just returns null.
On your callback handling method, in case of IdentityServer, on the ExternalController you read out the external identity of the temporary cookie - the result (in case of a success) holds both the Access & Refresh Token as Properties:
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result.Succeeded != true)
throw new Exception("External authentication error");
var tokens = result.Properties.GetTokens();
if (tokens?.Any() == true)
{
_logger.LogDebug("External authentication success resulted in provided access tokens: \r\n {0}.", string.Join(",", tokens.Select(d => $"{d.Name}:{d.Value}")));
}

AuthenticationFailedContext.Success() has no effect when handling JwtBearerEvents.OnAuthenticationFailed

I have an ASP.NET Core Web API targeting 2.2, and I'm having trouble handling an authentication request in the event that it has failed. The authentication request needs to pass a valid refresh token. My code is handling the failed event when the JWT has expired (I also provide a lifetime validator), and then issue a new JWT and refresh token when the provided refresh token is correctly validated, and thus consumed. However, the pipeline does not continue executing and instead returns a 403 regardless of my code calling Success() on the context.
Here is the relevant Startup.cs section:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:JwtSecret"])),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
LifetimeValidator = l10n.Api.Code.Auth.Bearer.JwtBearerExtensions.LifetimeValidator
};
options.Authority = Configuration["Authentication:JwtAuthority"];
options.ClaimsIssuer = Configuration["Authentication:JwtIssuer"];
options.Events = new JwtBearerEvents();
options.Events.OnAuthenticationFailed = async context => await context.AuthenticationFailed();
})
And the fail handler:
public static async Task AuthenticationFailed(this AuthenticationFailedContext context)
{
ILoggerFactory loggerFactory = context.HttpContext.RequestServices.GetService<ILoggerFactory>();
ILogger logger = loggerFactory.CreateLogger(nameof(JwtBearerExtensions));
string refreshToken = context.HttpContext.Request.Cookies[Defaults.RefreshTokenCookie];
if (string.IsNullOrEmpty(refreshToken))
{
logger.LogWarning("No refresh token supplied with invalid JWT, Cookies are {0}", string.Join(", ", context.HttpContext.Request.Cookies.Keys));
return;
}
logger.LogInformation("Processing refresh token '{0}'.", refreshToken);
IConfiguration configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
IUserService userService = context.HttpContext.RequestServices.GetService<IUserService>();
ITokenHandler handler = context.HttpContext.RequestServices.GetService<ITokenHandler>();
long? userId = await handler.ValidateRefreshToken(refreshToken);
if (userId.HasValue)
{
User user = await userService.GetUserAsync(userId.Value);
refreshToken = await handler.GenerateRefreshToken(userId.Value);
string jwtToken = BuildJwtToken(user, configuration);
context.HttpContext.Response.AddBearerAuthorization(jwtToken);
context.HttpContext.Response.AddRefreshTokenCookie(refreshToken);
context.Principal = new ClaimsPrincipal(BuildClaimsIdentity(user));
context.Success();
}
}
When inspecting the 403 result in Postman, I can see the new JWT in the Authorization header, and the new refresh token in the cookie. Those are correctly attributed to the Response object. It's just the pipeline that abandons the rest of the processing, and my controller action is never invoked.
How do I allow the request to continue and complete, returning the expected JSON and also issuing (or refreshing) a new auth session?
The solution was to use the correct constructor overload for the ClaimIdentity by passing in the authentication type. More information at this github issue.
For those interested, in this solution, the 403 result happened because of app.UseAuthorization(), in which a policy was failing to pass as a result of the authentication issue. In the github issue post, the purpose-built repro did not have this and resulted in the 401 instead.

Still logged in MVC site, but can't call web API

I have an ASP.NET MVC site, IdentityServer4 host and a web API.
When I log in the MVC site, using external provider (Facebook), I'm logged in fine. From the MVC site I can also consume the web API correctly.
However, the next day, I'm still logged in into the MVC site, but when I then try to access the web API, I get a 'not authorized exception'.
So although I'm still logged in in the MVC site, I'm not authenticated anymore to call a web API from within the MVC site.
I'm wondering how to handle this situation, and how IdentityServer4 should be configured.
Why am I still logged in the MVC site a day later? How can this be configured?
Why can't I still call the web API, if I'm still logged in the MVC site?
Can I sync the expiration times? Or how should I handle this?
The MVC application is configured like:
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = mgpIdSvrSettings.ClientId;
options.ClientSecret = mgpIdSvrSettings.ClientSecret; // Should match the secret at IdentityServer
options.ResponseType = "code id_token"; // Use hybrid flow
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("mgpApi");
options.Scope.Add("offline_access");
});
So it's using hybrid flow.
In IdentityServer the MVC client is configured like:
new Client
{
EnableLocalLogin = false,
ClientId = "mgpPortal",
ClientName = "MGP Portal Site",
AllowedGrantTypes = GrantTypes.Hybrid,
// where to redirect to after login
RedirectUris = mgpPortalSite.RedirectUris,
// where to redirect to after logout
PostLogoutRedirectUris = mgpPortalSite.PostLogoutRedirectUris,
// secret for authentication
ClientSecrets = mgpPortalSite.ClientSecrets.Select(cs => new Secret(cs.Sha256())).ToList(),
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"mgpApi"
},
AllowOfflineAccess = true,
RequireConsent = false,
},
And finally the web API:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = mgpIdSvrSettings.Authority;
options.RequireHttpsMetadata = false;
options.ApiName = mgpIdSvrSettings.ApiName;
options.EnableCaching = true;
options.CacheDuration = TimeSpan.FromMinutes(10);
});
There are two types of authentication, cookie and bearer.
Where the cookie keeps you logged in, the bearer token can't. Because the bearer token is set to expire at some point, without allowing you to change the lifetime.
The only way to access the resource (api) after the access token expires is to either let the user login again or request a new access token using a refresh token, without needing user interaction.
You've already configured it:
options.Scope.Add("offline_access");
On each login the request will at least contain a refresh token. Store it at a safe place and use it when needed. By default it is set to one time use only.
You can use something like this code to renew the token (as you are not actually refreshing it, but rather replacing it). You'll need to include the 'IdentityModel' NuGet package, as seen in the samples from IdentityServer.
private async Task<TokenResponse> RenewTokensAsync()
{
// Initialize the token endpoint:
var client = _httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError) throw new Exception(disco.Error);
// Read the stored refresh token:
var rt = await HttpContext.GetTokenAsync("refresh_token");
var tokenClient = _httpClientFactory.CreateClient();
// Request a new access token:
var tokenResult = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "mvc",
ClientSecret = "secret",
RefreshToken = rt
});
if (!tokenResult.IsError)
{
var old_id_token = await HttpContext.GetTokenAsync("id_token");
var new_access_token = tokenResult.AccessToken;
var new_refresh_token = tokenResult.RefreshToken;
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
// Save the information in the cookie
var info = await HttpContext.AuthenticateAsync("Cookies");
info.Properties.UpdateTokenValue("refresh_token", new_refresh_token);
info.Properties.UpdateTokenValue("access_token", new_access_token);
info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
return tokenResult;
}
return null;
}
By default the refresh token usage is configured as one time use. Please note that when storing the new refresh token fails and you should lose it, then the only way to request a new refresh token is to force the user to login again.
Also note that the refresh token can expire.
And taking it one step back, you'll need to use this when the access token expired or is about to expire:
var accessToken = await HttpContext.GetTokenAsync("access_token");
var tokenHandler = new JwtSecurityTokenHandler();
var jwtSecurityToken = tokenHandler.ReadJwtToken(accessToken);
// Depending on the lifetime of the access token.
// This is just an example. An access token may be valid
// for less than one minute.
if (jwtSecurityToken.ValidTo < DateTime.UtcNow.AddMinutes(5))
{
var responseToken = await RenewTokensAsync();
if (responseToken == null)
{
throw new Exception("Error");
}
accessToken = responseToken.AccessToken;
}
// Proceed, accessToken contains a valid token.

Use multiple JWT Bearer Authentication

Is it possible to support multiple JWT Token issuers in ASP.NET Core 2?
I want to provide an API for external service and I need to use two sources of JWT tokens - Firebase and custom JWT token issuers. In ASP.NET core I can set the JWT authentication for Bearer auth scheme, but only for one Authority:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
}
I can have multiple issuers and audiences, but I can't set several Authorities.
You can totally achieve what you want:
services
.AddAuthentication()
.AddJwtBearer("Firebase", options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-project"
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "my-firebase-project"
ValidateAudience = true,
ValidAudience = "my-firebase-project"
ValidateLifetime = true
};
})
.AddJwtBearer("Custom", options =>
{
// Configuration for your custom
// JWT tokens here
});
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
});
Let's go through the differences between your code and that one.
AddAuthentication has no parameter
If you set a default authentication scheme, then on every single request the authentication middleware will try to run the authentication handler associated with the default authentication scheme. Since we now have two possible authentication schemes, there's no point in running one of them.
Use another overload of AddJwtBearer
Every single AddXXX method to add an authentication has several overloads:
One where the default authentication scheme associated with the authentication method is used, as you can see here for cookies authentication
One where you pass, in addition to the configuration of the options, the name of the authentication scheme, as on this overload
Now, because you use the same authentication method twice but authentication schemes must be unique, you need to use the second overload.
Update the default policy
Since the requests won't be authenticated automatically anymore, putting [Authorize] attributes on some actions will result in the requests being rejected and an HTTP 401 will be issued.
Since that's not what we want because we want to give the authentication handlers a chance to authenticate the request, we change the default policy of the authorization system by indicating both the Firebase and Custom authentication schemes should be tried to authenticate the request.
That doesn't prevent you from being more restrictive on some actions; the [Authorize] attribute has an AuthenticationSchemes property that allows you to override which authentication schemes are valid.
If you have more complex scenarios, you can make use of policy-based authorization. I find the official documentation is great.
Let's imagine some actions are only available to JWT tokens issued by Firebase and must have a claim with a specific value; you could do it this way:
// Authentication code omitted for brevity
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase", "Custom")
.Build();
options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("Firebase")
.RequireClaim("role", "admin")
.Build());
});
You could then use [Authorize(Policy = "FirebaseAdministrators")] on some actions.
A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed. Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
This is an extension of Mickaƫl Derriey's answer.
Our app has a custom authorization requirement that we resolve from an internal source. We were using Auth0 but are switching to Microsoft Account authentication using OpenID. Here is the slightly edited code from our ASP.Net Core 2.1 Startup. For future readers, this works as of this writing for the versions specified. The caller uses the id_token from OpenID on incoming requests passed as a Bearer token. Hope it helps someone else trying to do an identity authority conversion as much as this question and answer helped me.
const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);
string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
.AddJwtBearer(Auth0, options =>
{
options.Authority = domain;
options.Audience = "https://myAuth0Audience.com";
})
.AddJwtBearer(MsaOpenId, options =>
{
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "00000000-0000-0000-0000-000000000000",
ValidateIssuer = true,
ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
ClockSkew = TimeSpan.FromMinutes(10),
};
options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
}
);
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes( Auth0, MsaOpenId )
.Build();
var approvedPolicyBuilder = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(Auth0, MsaOpenId)
;
approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));
options.AddPolicy("approved", approvedPolicyBuilder.Build());
});
The solution to your question, is available in following blog post https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
Basically the solutions exists of overriding the regular JWTBearer handler with you own generic handler that can check through the JWTBearerConfig if the issuer in the cfg is the same to the isseur in your token.
The blog post suggests to use seperate handlers for each scheme, that doesn't seem to be needed, a generic class JWTAuthenticationHandler that overrides the HandleAuthenticateAsync method seems to suffice!
Code wise you could implement your startup like this:
//Using multiple schemes can cause issues when validating the issuesSigningKey therefore we need to implement seperate handlers for each scheme! => cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
services.AddAuthentication()
//Set the authenticationScheme by using the identityServer helper methods (we are using a Bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
{
//TO DO Get the origin url's from configuration file, instead of setting all url's here
options.Authority = _identityServerSettings.Authority;
options.Audience = _identityServerSettings.Audience;
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
})
//Set the authentication scheme for the AzureAd integration (we are using a bearer token)
.AddScheme<JwtBearerOptions, JWTAuthenticationHandler>("AzureAD", "AzureAD", options =>
{
options.Audience = _azureAdSettings.Audience; //ClientId
options.Authority = _azureAdSettings.Authority; //"https://login.microsoftonline.com/{tenantId}/v2.0/"
options.TokenValidationParameters = new TokenValidationParameters
{
//Set built in claimTypes => Role
RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
//When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
//Cfr => https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
//A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
//We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnMessageReceived = context =>
{
return Task.CompletedTask;
},
OnForbidden = context =>
{
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
return Task.CompletedTask;
}
};
});
}
The JWTAuthenticationHandlerClass can look like this
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace WebAPI.Auth
{
public class JWTAuthenticationHandler: JwtBearerHandler
{
public JWTAuthenticationHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//Fetch OIDC configuration for the IDP we are handling
var authorityConfig = await this.Options.ConfigurationManager.GetConfigurationAsync(this.Context.RequestAborted);
//Determine the issuer from the configuration
var authorityIssuer = authorityConfig.Issuer;
var jwtToken = this.ReadTokenFromHeader();
var jwtHandler = new JwtSecurityTokenHandler();
//Check if we can read the token as a valid JWT, if not let the JwtBearerHandler do it's thing...
if (jwtHandler.CanReadToken(jwtToken))
{
//Read the token and determine if the issuer in config is the same as the one in the token, if this is true we know we want to let the JwtBearerHandler continue, if not we skip and return noResult
//This way the next IDP configuration will pass here until we find a matching issuer and then we know that is the IDP we are dealing with
var token = jwtHandler.ReadJwtToken(jwtToken);
if (string.Equals(token.Issuer, authorityIssuer, StringComparison.OrdinalIgnoreCase))
{
return await base.HandleAuthenticateAsync();
}
else
{
// return NoResult since the issuer in cfg did not match the one in the token, so no need to proceed to tokenValidation
this.Logger.LogDebug($"Skipping jwt token validation because token issuer was {token.Issuer} but the authority issuer is: {authorityIssuer}");
return AuthenticateResult.NoResult();
}
}
return await base.HandleAuthenticateAsync();
}
//Fetch the bearer token from the authorization header on the request!
private string ReadTokenFromHeader()
{
string token = null;
string authorization = Request.Headers["Authorization"];
//If we don't find the authorization header return null
if (string.IsNullOrEmpty(authorization))
{
return null;
}
//get the token from the auth header
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
return token;
}
}
}
One thing that was missing in Mickael's answer is that scheme needs to be specified in Authorize attribute (If you want to use authorization)
[Authorize(AuthenticationSchemes = "Firebase,Custom", Policy ="FirebaseAdministrators")]
Without AuthenticationSchemes provided, and AddAuthentication() has no parameter, NetCore fails to Authenticate and Request.HttpContext.User.Identity.IsAuthenticated is set to false

Categories