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

后端 未结 5 467
梦如初夏
梦如初夏 2021-01-30 13:46

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

相关标签:
5条回答
  • 2021-01-30 14:15

    After many hours of research and head-scratching, this is what worked for me in ASP.NET Core 2.2:

    • 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();
    } );
    
    0 讨论(0)
  • 2021-01-30 14:19

    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();
    }
    
    0 讨论(0)
  • 2021-01-30 14:23

    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";
    }
    
    0 讨论(0)
  • 2021-01-30 14:26

    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!

    0 讨论(0)
  • 2021-01-30 14:37

    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.
    }
    
    0 讨论(0)
提交回复
热议问题