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 th
After many hours of research and head-scratching, this is what worked for me in ASP.NET Core 2.2:
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();
}
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";
}
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!
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.
}