How to use 2 cookies for auth? - c#

I have 2 devices, pc and a special tablet. I want to have with same app .net core 2.0 2 cookies, or cookies scheme with 2 auth cookies, because for pc I want to expire in 5 minutes and for that special tablet to not expire at all. How to do that. Now I have this...
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "myScheme",
ExpireTimeSpan = TimeSpan.FromSeconds(300),
CookiePath = "/",
CookieSecure = env.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always
});
I think I could use UseWhen method or I don't know...
Or to sign in on different cookies?

Check Using cookie authentication without ASP.NET Core Identity
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/auth";
//https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.cookiebuilder?view=aspnetcore-2.1
options.Cookie = new CookieBuilder
{
Name = "CustomCookie",
HttpOnly = false
};
});
}
public async void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
}

Related

Blazor Server cookie authentication with custom AuthenticationScheme

I'm trying to build custom cookie authentication in my Blazor Server app.
It works as long as I use the DefaultAuthenticateScheme like this:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
});
Calling HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); will log me.
But I'd like to use custom AuthenticationSchemes to be able to have multiple schemes like:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("Attendee", options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
}).AddCookie("Admin", options =>
{
options.LoginPath = "/admin/login";
options.LogoutPath = "/admin/logout";
});
Calling HttpContext.SignInAsync("Admin", new ClaimsPrincipal(claimsIdentity), authProperties); do set the cookie, but still my app tells me that I'm not authorized.
<AuthorizeView>
<Authorized>Logged in!</Authorized>
<NotAuthorized>NOT logged in!</NotAuthorized> <!-- This is shown -->
</AuthorizeView>
I'd like to be able to control the access with #attribute [Authorize(AuthenticationSchemes = "Admin")] or #attribute [Authorize(Roles = "Admin")] on each component.
What could I be missing?
Your additional cookies aren't being used for authentication.
You could write your own authorization handler/middleware to do that, but by default, I think you can only use one cookie and you set it's name in this line of your code.
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
So, in brief, it's saying you're not authorized because it's testing the scheme CookieAuthenticationDefaults.AuthenticationScheme and not one of your additional two cookies.

ASP .NET Core 2.0 - OpenId Connect Auth : Correlation error

I am trying to create an authentication on an ASP.NET Core 2.0 web app.
My company is using Ping Federate and I am trying to authenticate my users using the company login page and in return validating the returned token using my signing key (X509SecurityKey down here).
The ping login link link looks like:
https://auth.companyname.com
I configured the Startup.cs to be able to log in and challenge against this site.
I decorated my HomeController with a [Authorize(Policy="Mvc")].
I am able to reach the login page, but, whenever I return from it I get (
I tried turning off/on multiple multiple validations):
Exception: Correlation failed.
Unknown location
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
The error message is not very helpful... anybody encountered such an issue before?
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = PF_LOGINPATH;
options.ClientId = Configuration["ClientId"];
options.ClientSecret = Configuration["ClientSecret"];
options.Scope.Clear();
options.ResponseType = OpenIdConnectResponseType.CodeIdTokenToken;
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = false;//true;
options.TokenValidationParameters = new TokenValidationParameters
{
RequireSignedTokens = false,
ValidateActor = false,
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = false,
ValidateTokenReplay = false,
// Compensate server drift
ClockSkew = TimeSpan.FromHours(24),
//ValidIssuer = PF_LOGINPATH;
// Ensure key
IssuerSigningKey = CERTIFICATE,
// Ensure expiry
RequireExpirationTime = false,//true,
ValidateLifetime = false,//true,
// Save token
SaveSigninToken = false
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Mvc", policy =>
{
policy.AuthenticationSchemes.Add(OpenIdConnectDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
}
// 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.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
I had similar situation. My Application url is like this : "https://domain/appname"
so when someone types url "https://domain/appname/" [with trailling slash], it gives Correlation error. This is how I have resolved it (found from some oher site)
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
//Auth schemes here
})
.AddOpenIdConnect(oid =>
{
//Other config here
oid.Events = new OpenIdConnectEvents()
{
OnRemoteFailure = OnRemoteFailure
};
});
}
private Task OnRemoteFailure(RemoteFailureContext context)
{
if (context.Failure.Message.Contains("Correlation failed"))
{
context.Response.Redirect("/AppName"); // redirect without trailing slash
context.HandleResponse();
}
return Task.CompletedTask;
}
end your callback url with a slash

Session timeout with azure Ad .net core 2.0

I am trying to authenticate .net core 2.0 application with the Azure ad. I got it successful with authentication. But I need to session timeout after idle time.
Please find my startup.cs config
Configure
logger.AddConsole(Configuration.GetSection("Logging"));
logger.AddDebug((category, logLevel) => (logLevel >= LogLevel.Trace));
app.UseResponseCaching();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseSession();
app.UseAuthentication();
ConfigureServices
services.AddAuthentication(options =>
{
options.DefaultScheme= CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
options.ClientId = Configuration["Authentication:AzureAd:ClientId"];
options.Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"];
options.ClientSecret = Configuration["Authentication:ClientSecret"];
options.CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"];
options.ResponseType = OpenIdConnectResponseType.IdToken;
})
.AddCookie();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(1);
options.CookieHttpOnly = true;
});
As the section Implementation Details under Working with Session State states as follows:
The server uses the IdleTimeout property to determine how long a session can be idle before its contents are abandoned. This property is independent of the cookie expiration. Each request that passes through the Session middleware (read from or written to) resets the timeout.
I enabled the session state, then set session values in an action and read them in another action. Per my test, your configuration for AddSession would issue a cookie with the default name .AspNetCore.Session and contains the session ID to the browser. The IdleTimeout is 1 minute and if you read or update the session values, then the IdleTimeout would be reset.
UPDATE:
AFAIK, there is no SessionEvents under SessionOptions when using services.AddSession. Per my understanding, you could set the Cookie expire time when using cookie auth, then add the processing to remove the session values and send the sign-out request to AAD when the cookie is invalid. Here is my configuration, you could refer to it as follows:
public void ConfigureServices(IServiceCollection services)
{
// Add MVC services to the services container.
services.AddMvc();
// Add Authentication services.
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
// Configure the OWIN pipeline to use OpenID Connect auth.
.AddOpenIdConnect(option =>
{
option.ClientId = Configuration["AzureAD:ClientId"];
option.Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAd:Tenant"]);
option.SignedOutRedirectUri = Configuration["AzureAd:PostLogoutRedirectUri"];
option.Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
};
})// Configure the OWIN pipeline to use cookie auth.
.AddCookie(op => {
op.ExpireTimeSpan = TimeSpan.FromMinutes(20);
op.LoginPath = "/Account/Login";
op.Events.OnRedirectToLogin =async(context) =>
{
//Clean the session values
context.HttpContext.Session.Clear();
//Sign-out to AAD
await context.HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
//Redirect to op.LoginPath ("/Account/Login") for logging again
context.Response.Redirect(context.RedirectUri);
};
});
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20);
options.CookieHttpOnly = true;
});
}

ASP.NET Core 2.0 combining Cookies and Bearer Authorization for the same endpoint

I've created a new ASP.NET Core Web Application project in VS17 using the "Web Application (Model-View-Controller)" template and ".Net Framework" + "ASP.NET Core 2" as the configuration. The authentication config is set to "Individual User Accounts".
I have the following sample endpoint:
[Produces("application/json")]
[Route("api/price")]
[Authorize(Roles = "PriceViwer", AuthenticationSchemes = "Cookies,Bearer")]
public class PriceController : Controller
{
public IActionResult Get()
{
return Ok(new Dictionary<string, string> { {"Galleon/Pound",
"999.999" } );
}
}
"Cookies,Bearer" is derived by concatenating CookieAuthenticationDefaults.AuthenticationScheme and JwtBearerDefaults.AuthenticationScheme.
The objective is to be able to configure the authorization for the end point so that it's possible access it using both the token and cookie authentication methods.
Here is the setup I have for Authentication in my Startup.cs:
services.AddAuthentication()
.AddCookie(cfg => { cfg.SlidingExpiration = true;})
.AddJwtBearer(cfg => {
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters() {
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
So, when I try to access the endpoint using a browser, I get the 401 response with a blank html page.
Then I login and when I try to access the endpoint again, I get the same response.
Then, I try to access the endpoint by specifying the bearer token. And that returns the desired result with the 200 response.
So then, if I remove [Authorize(AuthenticationSchemes = "Cookies,Bearer")], the situation becomes the opposite - cookie authentication works and returns 200, however the same bearer token method as used above doesn't give any results and just redirect to the default AspIdentity login page.
I can see two possible problems here:
1) ASP.NET Core doesn't allow 'combined' authentication.
2) 'Cookies' is not a valid schema name. But then what is the right one to use?
Please advise. Thank you.
If I understand the question correctly then I believe that there is a solution. In the following example I am using cookie AND bearer authentication in a single app. The [Authorize] attribute can be used without specifying the scheme, and the app will react dynamically, depending on the method of authorization being used.
services.AddAuthentication is called twice to register the 2 authentication schemes. The key to the solution is the call to services.AddAuthorization at the end of the code snippet, which tells ASP.NET to use BOTH schemes.
I've tested this and it seems to work well.
(Based on Microsoft docs.)
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
options.ClientId = "WebApp";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("api");
options.SaveTokens = true;
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api";
});
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
EDIT
This works for authenticated users, but simply returns a 401 (unauthorized) if a user has not yet logged in.
To ensure that unauthorized users are redirected to the login page, add the following code to the Configure method in your Startup class. Note: it's essential that the new middleware is placed after the call the app.UseAuthentication().
app.UseAuthentication();
app.Use(async (context, next) =>
{
await next();
var bearerAuth = context.Request.Headers["Authorization"]
.FirstOrDefault()?.StartsWith("Bearer ") ?? false;
if (context.Response.StatusCode == 401
&& !context.User.Identity.IsAuthenticated
&& !bearerAuth)
{
await context.ChallengeAsync("oidc");
}
});
If you know a cleaner way to achieve this redirect, please post a comment!
After many hours of research and head-scratching, this is what worked for me in ASP.NET Core 2.2 -> ASP.NET 5.0:
Use .AddCookie() and .AddJwtBearer() to configure the schemes
Use a custom policy scheme to forward to the correct Authentication Scheme.
You do not need to specify the scheme on each controller action and will work for both. [Authorize] is enough.
services.AddAuthentication( config =>
{
config.DefaultScheme = "smart";
} )
.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
// You could also check for the actual path here if that's your requirement:
// eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
if ( bearerAuth )
return JwtBearerDefaults.AuthenticationScheme;
else
return CookieAuthenticationDefaults.AuthenticationScheme;
};
} )
.AddCookie( CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = new PathString( "/Account/Login" );
options.AccessDeniedPath = new PathString( "/Account/Login" );
options.LogoutPath = new PathString( "/Account/Logout" );
options.Cookie.Name = "CustomerPortal.Identity";
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays( 1 ); //Account.Login overrides this default value
} )
.AddJwtBearer( JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey( key ),
ValidateIssuer = false,
ValidateAudience = false
};
} );
services.AddAuthorization( options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder( CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme )
.RequireAuthenticatedUser()
.Build();
} );
I think you don't need to set the AuthenticationScheme to your Controller. Just use Authenticated user in ConfigureServices like this:
// requires: using Microsoft.AspNetCore.Authorization;
// using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
For Documentation of my sources: registerAuthorizationHandlers
For the part, whether the scheme-Key wasn't valid, you could use an interpolated string, to use the right keys:
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}")]
Edit:
I did further research and came to following conclusion:
It's not possible to authorize a method with two Schemes Or-Like, but you can use two public methods, to call a private method like this:
//private method
private IActionResult GetThingPrivate()
{
//your Code here
}
//Jwt-Method
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
return GetThingsPrivate();
}
//Cookie-Method
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
return GetThingsPrivate();
}
If you have tried the above answers and they are not working for you, try replacing CookieAuthenticationDefaults.AuthenticationScheme with "Identity.Application":
services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
// Your bearer token configuration here
});
//Note how there is no `AddCookie` method here
services.AddAuthorization(options => {
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "Identity.Application")
.Build();
});
Then you can simply use [Authorize] on your controllers and the app will automatically decide whether to use Bearer or Cookie authentication:
[Authorize]
[ApiController]
[Route("api/something")]
public class MyController : ControllerBase {
// ...
And finally, you can configure the cookie properties (like sliding expiration or login path) using services.ConfigureApplicationCookie:
services.ConfigureApplicationCookie(options => {
options.SlidingExpiration = true;
//Return a 401 error instead of redirecting to login
options.Events.OnRedirectToLogin = async context => {
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes("Cookies: 401 unauthorized"));
};
//Return a 403 error instead of redirecting to login
options.Events.OnRedirectToAccessDenied = async context => {
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes("Cookies: 403 forbidden"));
};
});
Essentially this is all because using signInManager to log in uses the .ASpNetCore.identity.Application cookie rather than the standard cookies.
Credit goes to user Mashtani on this answer.
I was stuck for hours and hours on this going round in circles with 401 errors before I stumbled across the solution.
Tested with Asp.net Core 2.2
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
// name of the API resource
options.Audience = "api";
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:4991";
options.RequireHttpsMetadata = false;
options.ClientId = "WebApp";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("api");
options.SaveTokens = true;
});
services.AddAuthorization(options =>
{
// Add policies for API scope claims
options.AddPolicy(AuthorizationConsts.ReadPolicy,
policy => policy.RequireAssertion(context =>
context.User.HasClaim(c =>
((c.Type == AuthorizationConsts.ScopeClaimType && c.Value == AuthorizationConsts.ReadScope)
|| (c.Type == AuthorizationConsts.IdentityProviderClaimType))) && context.User.Identity.IsAuthenticated
));
// No need to add default policy here
});
app.UseAuthentication();
app.UseCookiePolicy();
In the controller, add necessary Authorize attribute
[Authorize(AuthenticationSchemes = AuthorizationConsts.BearerOrCookiesAuthenticationScheme, Policy = AuthorizationConsts.ReadPolicy)]
Here is the helper class
public class AuthorizationConsts
{
public const string BearerOrCookiesAuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme + "," + IdentityServerAuthenticationDefaults.AuthenticationScheme;
public const string IdentityProviderClaimType = "idp";
public const string ScopeClaimType = "scope";
public const string ReadPolicy = "RequireReadPolicy";
public const string ReadScope = "data:read";
}
I had a scenario where I need to use Bearer or Cookie only for file download api alone. So following solution works for me.
Configure services as shown below.
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(options =>
{
options.Authority = gatewayUrl;
})
.AddOpenIdConnect(options =>
{
// Setting default signin scheme for openidconnect makes it to force
// use cookies handler for signin
// because jwthandler doesnt have SigninAsync implemented
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://youridp.com";
options.ClientId = "yourclientid";
options.CallbackPath = "/signin-oidc";
options.ResponseType = OpenIdConnectResponseType.Code;
});
Then configure your controller as shown below.
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer,OpenIdConnect")]
public async Task<IActionResult> Download([FromQuery(Name = "token")] string token)
{
///your code goes here.
///My file download api will work with both bearer or automatically authenticate with cookies using OpenidConnect.
}
Christo Carstens, answer worked perfectly for me.
Just thought I'd share an additional check that I added to his AddPolicyScheme. (see above)
In my case the issue was that I had an Azure Web Service that was handling all my mobile app requests using JWT, but I also needed it to act as a gateway for Google/Apple/Facebook authentication which uses cookies.
I updated my startup as recommended
.AddPolicyScheme( "smart", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith( "Bearer " ) ?? false;
// You could also check for the actual path here if that's your requirement:
// eg: if (context.HttpContext.Request.Path.StartsWithSegments("/api", StringComparison.InvariantCulture))
if ( bearerAuth )
return JwtBearerDefaults.AuthenticationScheme;
else
return CookieAuthenticationDefaults.AuthenticationScheme;
};
} )
My only problem was that if a call was made to any of my api calls which had the [Authorize] attribute set, and no "Authorization" key was in the headers, then it would use Cookie authorization and return a Not found (404) instead of Unauthorized (401).
His suggestion to check for the Path worked, but I wanted to enforce JWT on any method which, in the future, may not have that path.
In the end I settled for this code.
.AddPolicyScheme("CookieOrJWT", "Bearer or Jwt", options =>
{
options.ForwardDefaultSelector = context =>
{
var bearerAuth = context.Request.Headers["Authorization"].FirstOrDefault()?.StartsWith("Bearer ") ?? false;
if (bearerAuth)
return JwtBearerDefaults.AuthenticationScheme;
else
{
var ep = context.GetEndpoint();
var requiresAuth = ep?.Metadata?.GetMetadata<AuthorizeAttribute>();
return requiresAuth != null
? JwtBearerDefaults.AuthenticationScheme
: CookieAuthenticationDefaults.AuthenticationScheme;
}
};
})
By checking the Endpoint metadata (only in rare cases where Authorization is not in the header), I can set JwtBearerDefaults.AuthenticationScheme for any method decorated with the [Authorize] attribute.
This works even if the method is inheriting the [Authorize] attribute from it's class and does not have it explicitly set.
e.g.
[ApiController]
[Route("api/[Controller]")]
[Authorize]
public class MyController : ControllerBase {
[HttpGet]
public ActionResult MyWebRequestThatRequiresAuthorization() {
return true;
}
}
Thanks to Christo Carstens for the solution. I was breaking my head over this. Saved me countless hours.

Why is ASP.NET Core Identity 2.0 Authorize filter causing me to get a 404?

I have a controller that I want to restrict only to a specific role, let's say admin. After setting a user with the admin role, I can validate that he's on that role using the IsInRoleAsync method (which returns true). When setting the attribute with [Authorize(Roles = "admin")] I get a 404 with that very same user . I'm using bearer tokens (I don't think that is relevant but anyway) and here's what I've done to try debugging:
Controller w/o [Authorize] : the resource is returned. [OK]
Controller with [Authorize] : the resource is returned only when I use the Authentication: Bearer [access token] [OK]
Controller with [Authorize(Roles = "admin")] : even after logging in with the user that has the role set, I get the 404 [NOK]
I don't know if I'm missing some configuration, but here's my ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddOpenIddict(opt =>
{
opt.AddEntityFrameworkCoreStores<ApplicationDbContext>();
opt.AddMvcBinders();
opt.EnableTokenEndpoint("/api/token");
opt.AllowPasswordFlow();
opt.DisableHttpsRequirement(); //for dev only!
opt.UseJsonWebTokens();
opt.AddEphemeralSigningKey();
opt.AllowRefreshTokenFlow();
opt.SetAccessTokenLifetime(TimeSpan.FromMinutes(5));
});
services.AddAuthentication(options =>
{
options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OAuthValidationConstants.Schemes.Bearer;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:44337/";
options.Audience = "resource_server";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = OpenIdConnectConstants.Claims.Subject,
RoleClaimType = OpenIdConnectConstants.Claims.Role
};
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// User settings
options.User.RequireUniqueEmail = true;
// Add application services.
options.ClaimsIdentity.UserNameClaimType= OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddSingleton(typeof(RoleManager<ApplicationUser>));
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
You likely get a 404 response because Identity - which is automatically configured as the default authentication, sign-in/sign-out and challenge/forbidden scheme by services.AddIdentity() - tries to redirect you to the "access denied page" (Account/AccessDenied by default), that probably doesn't exist in your application.
Try to override the default challenge/forbidden scheme to see if it fixes your issue:
services.AddAuthentication(options =>
{
// ...
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
});
To fix your second issue, make sure the JWT claims mapping feature is disabled. If it's not, the JWT handler will "convert" all your role claims to ClaimTypes.Role, which won't work as you configured it to use role as the role claim used by ClaimsPrincipal.IsInRole(...) (RoleClaimType = OpenIdConnectConstants.Claims.Role).
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// ...
options.SecurityTokenValidators.Clear();
options.SecurityTokenValidators.Add(new JwtSecurityTokenHandler
{
// Disable the built-in JWT claims mapping feature.
InboundClaimTypeMap = new Dictionary<string, string>()
});
});
I think that what you need is to check claims, not roles. Add an AuthorizeAttribute such as:
[Authorize(Policy = "AdminOnly")]
And then configure a policy that requires a claim:
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireClaim(OpenIdConnectConstants.Claims.Role, "Admin"));
});
Or, for debugging purposes or more advanced validation, you could have:
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireAssertion(ctx =>
{
//do your checks
return true;
}));
});

Categories

Resources