Trying to add Jwt authentification to my DotNetCore 2.1 Server and Angular 6 App.
I've seen so many articles on the topic and no one seem's to do it the same way and nothing seem's to work for me either... i dont know what's wrong...
i'm getting : 'The AuthorizationPolicy named: 'Bearer' was not found.' when i start my server...
Services
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "http://localhost:54523",
ValidAudience = "http://localhost:4300",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("tokensecret))
};
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.Build();
});
});
services.AddMvc();
Configurations
app.UseAuthentication();
app.UseCors("CorsPolicy");
app.UseMvc();
Controllers
[Authorize()]
[Route("api/[controller]")]
public class ProjectController : Controller
If i use the Controller [Authorize], when the user is not authentificated it return to /Account/Login?ReturnUrl=...
but it's JWT it should return 401, 403 only...
if i try with [Authorize(JwtBearerDefaults.AuthenticationScheme)] i'm getting 'The AuthorizationPolicy named: 'Bearer' was not found.'
but why...
EDIT
I didn't know that line was changeing the behaviour of authentification but I also use this line
serviceCollection.AddIdentity<User, Role>();
What's wrong ?
We cannot use Identity with JWT ?
how to configure it for JWT ?
Ok, I've found the way to get it working... finally!
You need to use AddIdentityCore instead of AddIdentity.
Then you need to configure it yourself and add the missings services that are not registered in AddIdentityCore.
link to AddIdentityCore method : https://github.com/aspnet/Identity/blob/9b385180a9abcb264507efc23279f083bfc50520/src/Core/IdentityServiceCollectionExtensions.cs
Identity Registration Code
var builder = serviceCollection.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = true;
opt.Password.RequiredLength = 8;
opt.Password.RequireNonAlphanumeric = true;
opt.Password.RequireUppercase = true;
opt.Password.RequireLowercase = true;
});
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<AmiliaContext>();
builder.AddDefaultTokenProviders();
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
serviceCollection.AddDependencies(Assembly.GetExecutingAssembly());
Additionnal Notes
User must inherit IdentityUser
Role must inherit IdentityRole
You must not use SignInAsync from the SignInManager, instead you need to use CheckPasswordSignInAsync.
Why ?
Because SignInAsync is using the cookie internaly so we cannot use this method in JWT.
Related
I'm trying to create an asp-mvc api that reads the jwt claims on an incoming request, and then mirrors them back to the user - my use case is that I'm trying to investigate why a different endpoint is failing, I get a token from an external service, so I want minimal/no auth on it, I just want to inspect the claims.
I have the following Controller and endpoint:
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
[Route("testclaims")]
public async Task<ActionResult<string?>> TestClaims()
{
List<string> result = new List<string>();
foreach (var claim in HttpContext.User.Claims)
result.Add($"{claim.Type}: {claim.Value}");
return "{ " + String.Join(", ", result.ToArray()) + " }";
}
}
My app is setup like this:
...
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
options => {
options.TokenValidationParameters = new TokenValidationParameters
{
RequireExpirationTime = false,
RequireSignedTokens = false,
RequireAudience = false,
SaveSigninToken = false,
TryAllIssuerSigningKeys = false,
ValidateActor = false,
ValidateAudience = false
};
builder.Configuration.Bind("JwtSettings", options);
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
I'm calling the endpoint from postman I make a request with an auth token containing a know set of claims, HttpContext.User.Claims is always empty, but I can see HttpContext.Request.Headers contains the expected token, and if I decode that externally, it contains the claims.
I've tried adding an Authorize decorator to the endpoint, but when I do I get a 401 returned with no log or debug info.
I'm assuming there's just something I need to enable to have asp populate the claims?
You need to use [Authorize].
Besides, you miss the app.UseAuthentication(); before your app.UseAuthorization();.
app.UseAuthentication();
app.UseAuthorization();
result:
Have a try, hope it can help you.
I have a configuration of JWT Bearer authentication, but sometimes instead of using JWT token, I want to use an API KEY in the request header and check this key in a middleware.
But in that case, when I don't put the bearer token in the header, I always respond with an Unauthorized response code.
How can I disable the bearer token check?
My configuration:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
// options.RequireHttpsMetadata = false;
// options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret)),
ValidIssuer = jwtSettings.Issuer,
ValidAudiences = jwtSettings.Audiences,
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
Rather than checking in a middleware a more idiomatic way you can achieve this by using multipe AuthenticationSchemes. See the MSDN link for more details but at a very high level you can assign add multiple authentication schemes, each with a different scheme. You then refer to this scheme name when using the autorize attribute (e.g. [Authorize(AuthenticationSchemes = "Api-Key-Scheme")]).
services
.AddAuthentication()
.AddJwtBearer(options => { .. })
.AddApiKey(options => { .. }); // custom code
The .AddApiKey() method above will require a custom AuthenticationHandler<T> implementation, an example of how to do that can be found here - https://josef.codes/asp-net-core-protect-your-api-with-api-keys/
You can use the [AllowAnonymous] attribute on your method to disable the authentication check.
Then, create an ActionFilterAttribute and apply it to the same method:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace YourNameSpace
{
public class RequireYourKeyHeader : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if(!filterContext.HttpContext.Request.Headers.TryGetValue("YourKey", out string headerValue))
{
filterContext.Result = new BadRequestObjectResult("missing header value");
}
// TODO: check if value passed in the header is actually valid
}
}
}
Apply with [RequireYourKeyHeader]
I'm trying to user AddJwtBearer to Authorize my API's.
I'm using this in my Startup.cs
services.AddControllers();
AddJwtBearer(options =>
{
options.IncludeErrorDetails = true;
options.MetadataAddress = Configuration["Issuer:uri"] + Configuration["Issuer:well-known"];
options.Authority = Configuration["Issuer:uri"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["Issuer:uri"],
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateAudience = false,
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
But I'm always getting the error : IDX10501: Signature validation failed. Unable to match key: \nkid: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' because the IssuerSigningKeys are always null inside the TokenValidationParameters. Is there a way to load the issuer signing keys from the metadata url provided without writing specific code for that?
thanks
Thanks for the quick reply, but I figured it out. I had 2 issues, 1) I has specifying the .well-known/openid-configuration/jwks endpoint rather then just .well-known/openid-configuration, this alone instantiated my ConfigurationManager correctly; 2) I had to add app.UseAuthentication(); before app.UseAuthorization(); and now my code is correctly validating the JWT Bearer token signature against the correct Identity Server.
I am working on a blazor application where I used my API project as Identity
Provider. Everything is working fine but the issue is that the access token
issued by my API is not validated by the API. It turns out the API is expecting a
cookie header. I took a closer look at blazor hosted application and found out
the cookie is being sent along with each request but it's same-origin.
My Blazor WASM project does not automatically attach this cookie in the request
header, just the access token.
Is there a way I can make the Http handler attach this cookie on each request?
or make the API validate the access token instead of the identity cookie.
This is my startup class in the API Project
public static void AddIdentityServer(IServiceCollection services,IConfiguration configuration)
{
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/Identity/Account/Login";
options.UserInteraction.LogoutUrl = "/Identity/Account/Logout";
}).AddProfileService<LocalProfileService>()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(option =>
{
option.Clients.Add(new Client
{
ClientId = "blazor",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedScopes = { "openid", "profile", "email","id" },
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
Enabled = true,
RequireConsent = false,
});
option.IdentityResources.AddEmail();
option.IdentityResources["openid"].UserClaims.Add("name");
option.ApiResources.Single().UserClaims.Add("name");
option.IdentityResources["openid"].UserClaims.Add("role");
option.ApiResources.Single().UserClaims.Add("role");
option.IdentityResources.Add(new IdentityResource("id",new string[] {"id" }));
option.ApiResources.Single().UserClaims.Add("id");
});
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = configuration["ExternalLoginApiKey:GoogleClientId"];
options.ClientSecret = configuration["ExternalLoginApiKey:GoogleClientSecret"];
})
.AddFacebook("Facebook", options =>
{
options.AppId = configuration["ExternalLoginApiKey:FacebookAppId"];
options.AppSecret = configuration["ExternalLoginApiKey:FacebookAppSecret"];
})
.AddIdentityServerJwt();
}
Program class in the Blazor Project
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("oidc", options.ProviderOptions);
options.UserOptions.RoleClaim = "role";
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
builder.Services.AddHttpClient<IAuthorizedRestService, AuthorizedRestService>(
client => client.BaseAddress = new Uri("https://localhost:5002/api/mart/v1/"))
.AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(authorizedUrls: new[] { "https://localhost:5002" }));
builder.Services.AddHttpClient("noauth", option => option.BaseAddress = new
Uri("https://localhost:5002/api/mart/v1/"));
builder.Services.AddScoped<IRestService, RestService>();
await builder.Build().RunAsync();
}
I have found the Solution.
It happens that there is already a JWT handler provided by IdentityServer4 for APIs that double as Authorization Server
.AddIdentityServerJwt();
So what I did was to configure it
services.Configure<JwtBearerOptions>
(IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = "https://localhost:5002";
options.Audience = "mart";
options.SaveToken = true;
});
Then specify the Authentication scheme to use
[Authorize(AuthenticationSchemes = IdentityServerJwtConstants.IdentityServerJwtBearerScheme)]
You can also add it globally in the start up class
var authorizationPolicy = new AuthorizationPolicyBuilder(IdentityServerJwtConstants.IdentityServerJwtBearerScheme)
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(authorizationPolicy));
You can read more using these links
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-3.1
I am attempting to learn how to use WS-Federation in a .NET Core application. I am attempting a basic step of having my client application redirect to an authentication service (defined in my FederationMetadata.xml file), but when I attempt to access a controller requiring authentication [url in this case is http://localhost/STSAwareApp/Test], my redirect url becomes too large to be usable (I get a 404.15, query string is too large).
Since I'm new to WS-Federation, I'm assuming that I have a configuration issue in my startup:
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddControllersWithViews();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddWsFederation(authenticationScheme: "WsFederation", displayName: "Test WS-Fed", options =>
{
options.Wtrealm = "http://localhost/STSAwareApp/Test";
options.MetadataAddress = "http://localhost/STSAwareApp/files/FederationMetadata.xml";
options.RequireHttpsMetadata = false;
}).AddCookie(options =>
{
options.Cookie.Name = "TestStsAuth";
options.Cookie.HttpOnly = true;
});
services.AddLogging(
builder =>
{
builder.AddFilter("Microsoft", LogLevel.Trace)
.AddFilter("System", LogLevel.Trace)
.AddConsole();
});
}
}
Here is an example of the redirect URL that is getting generated (just for completeness sake):
http://localhost:80/STSAwareApp/Test?wtrealm=http%3A%2F%2Flocalhost%2FSTSAwareApp%2FTest&wa=wsignin1.0&wreply=http%3A%2F%2Flocalhost%2FSTSAwareApp%2Fsignin-wsfed&wctx=CfDJ8O7dpxEY6MBCgxct4kkpp1gFIwYvsJN7p6zOuAiyltKmCqff605h1uCh7ZBNM6WneU_7XlxHKAt7CYmBdXG_e19L8z-p64d21gJjDJCdjOkfNieQWNRSPQPGZDUL8eBEVqs4vWaKN-sof8lnblDbySiP8NJPR945c8IYqRwaf7ZBZ-_IxoWZLN_OgMOgFnU5XjtDeUfFCcHh0dtGwSc4PVDPxhKIpxb3JyIEMBRA19qZpudqQEylX6WHek5LkNK1IDbWDv2ll9F5HCJSQxvpVDrLw62dBfF6IDNg3Ar8q2Yr_bpV1gA1RR7kHp3Gs4soxfZENfvi96qkPJs4ZOqvUYjRQjho34Lkc9VH5q2w7n4Oty6abFXs_jeDQQN7ZyFBGQrb-wxBZBEuvNJAFp-ckhGVCeKrtdmXS4bVAvbEtPAEtLHXJpv82Y843_UVCeAQycMjmz2stIovI-HiKAWwCkoc03J7gOlTEwyrn1cR-Ia3QWN4mPN2ncqxW5e80kamNDIDmRxiWoox1Z6x5SATSIO3KergXc7VE1G8-2gLicc8_flyLR6NXUAdDRZTnxGzChHzf2L1eqjm0K_PvioAdqJNuFDlFMeGyfarEbXahAqpchuDvSgolSEKgGO-uLw5GEdCS-5cX_Ztt3bAjbXzkPMdhzYbXFWTDdYTMMMta18nhzgAk5CIzDvo1BmniWGdwUy-lAWm9BoNd4TsroQa-F8NJ86K4sixQIqRqQ-D-Bf_672hHbIkY1QEEe8tqTH-1Qwn9K5RY5sVFQLu0Ec4bp0Zj2EDis-GAtMxhp6761MciYjjhqgORhe3gsLeej5GEY0AErXUOCxdghQKs-waLQtNQ2F4Xn226DYp6NVn8bLs5pu4mFblaWRn9cVzKPHUosRT9BjKqbnLpCCC0A4cOXec-G5znWLOXa6G4qsZjFl5h79MwStDnzP6GU2Wg6TaLG83783f6bRsJwX8blc1CMEjByphkpZp-VdR6FytLXvu4bh8gQQo2K3ad76pNlF8HnA4y1f0p86A82i2IPPDrOeW6YFupzZRITSFz-JvhjAZbkSzu26bgqgHNVTIz1ebu9mHIMQzGzpAu0rFIl16HszR7Omxn8TljADTCCLasQyLNRUIXSA5teeowULetXEv_rmOr6ANkk0kQ-q3pPuiOzkA0aFV6g1jYQ-JvS9K817IafEes7akoDrPbeHEmvD5sWzxERlMtnEQtYwcrPiOroWXIh1QgLjqUgTxtagWmkzoBWVM5PnNmMVkk0alyTgZKOomTcZN8ePkLRp4sY0d0D_uqb0Rn_s757Nb-oDztAz6SLOkCzWnPDif3eIAFTZy24v_oYr3SOFfvM2J-_t0kg3zlRovg25_bPPSs-qyfrMMBSbMammB5e7SKbIna4dPhMdv93Vm6I2GwJ8-VY-pAuBT4MQXPLD1VwdiBT3hWsZOoeMUl1JuL7B9pJDAMBNO2OUTaRb7dajP3VsA09XSgVrBeZ1Hvk733TrzFVoR5KQgHS4qw9cxquRmqP2XfEYTQocB-mUL4b-n0h3RN2qzaHn_VH2pZDV842YcanF4SZ8dDPB4EnLCWU7pf67IwvruInvu8MXg01xNoURh6rKLmSwikbgsEM7Es87RMQSEvar1QixBId9XMO1YiHVvGAdJoivUveJSO1T8Aj4A2xFllBjtD4SfnJc5UDTQ7UxGnVmIVw6pwS9N26U_u09n-T4j5R-ZVQyCNgSjoNRg-3jmMatXcAhT4vJgO-kRuzMiBKnavJ7EPyS8Th8KUK0ws1tQYQKmQQGvd7DT_GRC0wXT8HrTZ1uxTmxxDibzyCLxJZmulLHPcYaXwpWw6j56vOxgCrGy-3L5GtfnXNN1UdE3QzbE6_XL3xF8B3uD6Z5g5ZB_ZR4Q0QS0K9Kb6guaAtxEJYKc2eE2DZ2OpMNtyw5imNYt9crd5J4mB05GR7c0Nur2vqzk1mGM56_0IQD9L4HV4fXNmQuprEpwNZ41NyW-bhcVS30rZn73WLc-XBlNWhCrE_HiTWzCDOn9juofX7_C2AcQypJt-aweXEN5uxRWPp_W9qFJNblrkjzAEr3o7_dylYLYTstOvW4dYuIE4WlTUiJdJF3Iy02whGQUpclOINsxZ3wotkKY2JsnUzsolSeIfWe-es8soGOkPnDSthgjRbpTxltmVz10L0kAo4zckz4HvhEmziWPsGWZH1UVtRKLniT60qq9PPxeuu_dsodov-ByanyRwMHlkzCJhmSBDE0
I'm under the assumption that the url is not only incorrect because the wctx is too long, but it should be attempting to redirect to a different service [http://localhost/STS/V1], it is attempting to the original url with the federation parameters in the query string. I think that the wctx should be smaller, because even if I increase the size of my URL limits, it just continues to grow.
The issue with this was that the FederationMetadata.xml document was not configured correctly. Not exactly sure what was wrong, but instead of attempting to have a valid xml document, it was simpler to update the Configuration property of the WsFederationOptions. Attached is the updated Startup call:
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddControllersWithViews();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddWsFederation(authenticationScheme: "WsFederation", displayName: "Test WS-Fed", options =>
{
WsFederationConfiguration configuration = new WsFederationConfiguration();
configuration.TokenEndpoint = "http://localhost/STSSpike/V1";
options.Configuration = configuration;
options.Wtrealm = "http://localhost/STSAwareApp/Test";
}).AddCookie(options =>
{
options.Cookie.Name = "TestStsAuth";
options.Cookie.HttpOnly = true;
});
services.AddLogging(
builder =>
{
builder.AddFilter("Microsoft", LogLevel.Trace)
.AddFilter("System", LogLevel.Trace)
.AddConsole();
});
}
Guessing that this is a pretty localized issue, so might want to close.