.Net Core with WS-Federation generates large wctx - .net-core

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.

Related

Aspnet Core 3.1 MVC Razor pages gets 302 after authenticating with Identity Server 4 OpenIdConnect

I have an MVC Razor pages app which I want to hook into our Identity Server Implementation. We have followed the tutorial in their quickstart https://identityserver4.readthedocs.io/en/latest/quickstarts/2_interactive_aspnetcore.html and got it working in a brand new project, so we assume the client and IDS configuration is OK.
However, when we port it into our RazorPages application we get into a loop. We are sent off to the IDS, we log in and we're sent back to the signin-oidc page. This page seems to generate a 302.
Please see this network trace. Each time the request is made a new "code_challenge" parameter is requested
My startup is (sorry long and) here:
public void ConfigureServices( IServiceCollection services )
{
services.AddIdentity<ApplicationUser, IdentityRole>( options =>
{
options.SignIn.RequireConfirmedEmail = true;
} )
.AddEntityFrameworkStores<CourseRegContext>()
.AddDefaultTokenProviders();
var cs = Configuration.GetConnectionString( "DefaultConnection" );
var skipHTTPS = Configuration.GetValue<bool>( "LocalTest:skipHTTPS" );
services.Configure<MvcOptions>( options =>
{
if ( /*Environment.IsDevelopment() && */!skipHTTPS )
{
options.Filters.Add( new RequireHttpsAttribute() );
options.EnableEndpointRouting = false;
}
} );
services.AddMvc()
.SetCompatibilityVersion( CompatibilityVersion.Version_2_1 )
.AddNewtonsoftJson(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
services.AddMvc( config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add( new AuthorizeFilter( policy ) );
config.EnableEndpointRouting = false;
} ).AddRazorPagesOptions( options =>
{
options.Conventions.AllowAnonymousToFolder( "/Oops" );
options.Conventions.AuthorizeFolder( "/Test" );
} );
services.AddMemoryCache();
services.AddHttpsRedirection( options =>
{
options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
if ( Environment.IsDevelopment() )
{
options.HttpsPort = 44311;
}
else
{
options.HttpsPort = 443;
}
} );
services.AddHsts( options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromHours( 1 );
} );
AddAuthorisation( services );
}
private void AddAuthorisation( IServiceCollection services )
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication( options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
} )
.AddCookie( "Cookies" )
.AddOpenIdConnect( "oidc", options =>
{
options.Authority = "https://localhost:41012";
options.ClientId = "validId";
options.ClientSecret = "somesecret";
options.ResponseType = "code";
options.Scope.Add( "roles" );
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
//options.UsePkce = true;
} );
//services.AddAuthorization();
services.AddAuthorization( options =>
{
options.AddPolicy( AuthPolicies.GlobalAdmin, policy =>
policy.RequireRole( ApplicationRoles.Administrator ) );
options.AddPolicy( AuthPolicies.CourseAdmin, policy =>
policy.RequireRole(
ApplicationRoles.Administrator,
ApplicationRoles.CourseAdmin ) );
options.AddPolicy( AuthPolicies.Presenter, policy =>
policy.RequireRole( ApplicationRoles.CourseViewer ) );
options.AddPolicy( AuthPolicies.UserAdmin, policy =>
policy.RequireRole( ApplicationRoles.UserAdmin, ApplicationRoles.Administrator ) );
} );
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure( IApplicationBuilder app, IHostingEnvironment env )
{
if ( env.IsDevelopment() )
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler( "/Oops/C500" );
app.UseHsts( options => options.MaxAge( hours: 1 ).IncludeSubdomains() ); //todo when confident it is working, use 180 days
}
app.UseStatusCodePagesWithRedirects( "/Oops/C{0}" );
app.UseXXssProtection( options => options.EnabledWithBlockMode() );
app.UseXContentTypeOptions();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints( endpoints =>
{
//endpoints.MapDefaultControllerRoute()
//.RequireAuthorization();
} );
app.UseMvc();
}
I wonder if it is something do with MVC and EndPoints as part of the upgrade to .NET Core 3.1, but I am unsure how to keep the rest of the project working and yet still get the IDS integration.
I started a blank project and got it authenticating with no other code attached and it worked. So I then put each line back fixing each DI error until I saw the error again. It was to do with services.AddIdentity<ApplicationUser, IdentityRole> which I guess makes sense as it adds login authentication stuff as well as the managers. So, this line needs to be removed, but also need to remove all references to the usermanager etc too.
One mass refactor later and have now broken the redirect to IDS, but hopefully will get it redirecting. (it's now just acting like there is no authentication at all)

Automatically Attaching Identity Cookie to HTTP Client in Blazor wasm

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

How to make ASP.NET/React app serve SPA from subpath?

I have a stock aspnetcore and reactjs app, generated from the starter template (dotnet new react). I would like the SPA app to be served from a subpath off the root url; e.g. instead of the sample app being https://localhost:5001/counter I'm looking for it to instead be served from https://localhost:5001/myapp/counter.
I changed the Startup.cs from:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
to this:
app.Map(new Microsoft.AspNetCore.Http.PathString("/myapp"), appMember =>
{
appMember.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
});
This sort of works. If I browse to https://localhost:5001/myapp/ it appears to load the index.html, but the static files are attempting to load from the root path and not the subpath.
What needs to be changed so that the react app uses the subpath as the root? I'd like this to work both in the interactive VS dev environment and when deployed, likely on IIS. It seems like it's close but I'm missing something.
Sample demo of the solution is available here: https://github.com/petertirrell/mvc-spa-demo/tree/master/mvc-spa-demo
Thanks!
Start with moving app to sub-path by adding this to top of package.json:
"homepage": "/myapp/",
When running npm start inside ClientApp folder, app is now serving http://localhost:3000/myapp
Then change Startup.cs like this:
First remove
app.UseSpaStaticFiles()
then add
const string spaPath = "/myapp";
if (env.IsDevelopment())
{
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments(spaPath)
|| ctx.Request.Path.StartsWithSegments("/sockjs-node"),
client =>
{
client.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.UseReactDevelopmentServer(npmScript: "start");
});
});
}
else
{
app.Map(new PathString(spaPath), client =>
{
// `https://github.com/dotnet/aspnetcore/issues/3147`
client.UseSpaStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = ctx =>
{
if (ctx.Context.Request.Path.StartsWithSegments($"{spaPath}/static"))
{
// Cache all static resources for 1 year (versioned file names)
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(365)
};
}
else
{
// Do not cache explicit `/index.html` or any other files. See also: `DefaultPageStaticFileOptions` below for implicit "/index.html"
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(0)
};
}
}
});
client.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions()
{
OnPrepareResponse = ctx => {
// Do not cache implicit `/index.html`. See also: `UseSpaStaticFiles` above
var headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(0)
};
}
};
});
});
}
Don't forget to clear browser history before testing changes for the first time on e.g. Azure.
You can do so by having:
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
in your ConfigureServices and:
string spaPath = "/myapp";
if (env.IsDevelopment())
{
app.MapWhen(y => y.Request.Path.StartsWithSegments(spaPath), client =>
{
client.UseSpa(spa =>
{
spa.UseReactDevelopmentServer(npmScript: "start");
});
});
}
else
{
app.Map(new PathString(spaPath), client =>
{
client.UseSpaStaticFiles();
client.UseSpa(spa => {});
});
}
It should be noted that in development we use .MapWhen because .Map would cause your static files to be available at /myapp/myapp/[file] as opposed to /myapp/[file].
I combined some of each answer to get it working. You need to add the "homepage": "/myapp" to the package.json as well as the config changes to Startup.cs. I used the simpler config provided in Shoe's answer without all the extra caching and sockets directives, as I don't need those.
Because my application also used React Router for SPA routing under /myapp I also needed to add basename to the root BrowserRouter:
<BrowserRouter basename="/myapp" >...</BrowserRouter>

The AuthorizationPolicy named: 'Bearer' was not found

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.

How to validate a claim in id_token when using OpenIdConnect middleware?

I'm using Oath2 with Google authentication in my ASP.NET Core MVC app. I want to restrict logged in users to a certain G Suite domain which according to the docs is done using the "hd" (hosted domain) claim. I have it working but as it's authentication and I'm not familiar would like input. Am I doing this correctly? Is there a way to instead return a 401 status code instead of calling Fail() which results in a 500 error?
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddOpenIdConnect(o =>
{
var hostedDomain = new KeyValuePair<string, string>("hd", "mysite.com");
o.ClientId = "...";
o.ClientSecret = "...";
o.Authority = "https://accounts.google.com";
o.ResponseType = "id_token token";
o.Scope.Add("openid");
o.Scope.Add("email");
o.Scope.Add("profile");
o.GetClaimsFromUserInfoEndpoint = true;
o.SaveTokens = true;
o.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = (context) =>
{
// Add domain limiting using 'hd' or 'hosted domain' parameter
// Docs: https://developers.google.com/identity/protocols/OpenIDConnect#hd-param
//context.ProtocolMessage.SetParameter(hostedDomain.Key, "asdf.com");
// Set up redirect URLs
if (context.Request.Path != "/account/external")
{
context.Response.Redirect("/account/login");
context.HandleResponse();
}
return Task.FromResult(0);
},
OnTokenValidated = (c) =>
{
var hdClaim = c.SecurityToken.Claims.FirstOrDefault(claim => claim.Type == hostedDomain.Key);
if(hdClaim?.Value == null || hdClaim.Value != hostedDomain.Value)
{
// The claim is null or value is not the trusted google domain - do not authenticate!
c.Fail($"Invalid claim for '{hostedDomain.Key}'! User does not belong to trusted G Suite Domain");
}
return Task.FromResult(0);
}
};
});
services.AddMvc();
}
The above works when an incorrect or null hd claim is given which is done by logging in with an account not in the domain name in the hostedDomain.Value. I tried setting the c.Response.StatusCode = 401; but the user still logs in.
Another way to do this would be to use authorization.
You can set up a default authorization policy that requires the presence of the claim you test for. Then any caller that does not have the claim would get redirected to an access denied page. Something like:
services.AddAuthorization(o =>
{
o.AddPolicy("default", policy =>
{
policy.RequireAuthenticatedUser()
.RequireClaim("hd", "mysite.com");
});
});
services.AddMvc(o =>
{
o.Filters.Add(new AuthorizeFilter("default"));
});

Resources