aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 599 forks source link

[Draft] Auth 2.0 Migration announcement #1310

Closed HaoK closed 7 years ago

HaoK commented 7 years ago

Note: this issue is closed, you should use https://github.com/aspnet/Security/issues/1338

Summary:

The old 1.0 Authentication stack no longer will work, and is obsolete in 2.0. All authentication related functionality must be migrated to the 2.0 stack, any interop between old and new must be side by side apps, as opposed to mixing 1.0 auth code with 2.0 auth code in the same app. Cookie authentication will interop, so 1.0 Cookies and 2.0 Cookies will be valid in both apps if configured properly. The main motivation was to move to a more flexible service based IAuthenticationService and away from the old middleware/IAuthenticationManager design that came over from Microsoft.Owin.

IAuthenticationManager(aka httpContext.Authentication) is now obsolete

This was the main entry point into the old auth system. This has now been replaced with a new set of HttpContext extensions that live in the Microsoft.AspNetCore.Authentication namespace and remain very similar:

// Add using to pickup the new extension methods
using Microsoft.AspNetCore.Authentication;

// Update by just removing the .Authentication
context.Authentication.AuthenticateAsync => context.AuthenticateAsync
context.Authentication.ChallengeAsync => context.ChallengeAsync

Configure(): UseXyzAuthentication has been replaced by ConfigureService(): AddXyz()

In Auth 1.0, every auth scheme had its own middleware, and startup looked something like this:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseIdentity();
    app.UseCookieAuthentication(new CookieAuthenticationOptions
       { LoginPath = new PathString("/login") });
 Β   app.UseFacebookAuthentication(new FacebookOptions
       { AppId = Configuration["facebook:appid"],  AppSecret = Configuration["facebook:appsecret"] });
}Β 

In Auth 2.0, there is now only a single Authentication middleware, and each authentication scheme is registered during ConfigureServices, and UseIdentity() is no longer required (since it was just calling UseCookie 4 times underneath the covers)

public void ConfigureServices(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores();
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(o => o.LoginPath = new PathString("/login"))
                .AddFacebook(o =>
                {
                    o.AppId = Configuration["facebook:appid"];
                    o.AppSecret = Configuration["facebook:appsecret"];
                });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) {
    app.UseAuthentication();
}

New Microsoft.AspNetCore.Authentication.Core/Abstractions

All of the old Authentication namespaces in HttpAbstractions have been deprecated. The new Auth 2.0 stack lives in two new packages inside the HttpAbstractions repo: Microsoft.AspNetCore.Authentication.Core/Abstractions.

Brief overview:

Types that are mostly unchanged, just with new homes:

Security repo: Microsoft.AspNetCore.Authentication / AuthenticationHandler changes

All of the core abstractions and services for authentication live in HttpAbstracions, but there's an additional layer of base classes/functionality targeted towards implementation of AuthenticationHandlers. This is also where the AuthenticationMiddleware lives. The handlers themselves for the various implementations aren't drastically different, but there were a fair amount of changes

Microsoft.AspNetCore.Authentication

Overview:

Event changes overview

At a high level, there 3 main kinds of events:

  1. BaseContext events which are the simplest and just expose properties with no real control flow.

  2. ResultContext events which revolve around producing AuthenticateResults which expose:

    • Success(): used to indicate that authentication was successful and to use the Principal/Properties in the event to construct the result.
    • NoResult(): used to indicate no authentication result is to be returned.
    • Fail(): used to return a failure.
  3. HandleRequestContext events are used in the IAuthenticationRequestHandler/HandleRemoteAuthenticate methods and adds two more methods:

AutomaticAuthentication/Challenge have been replaced by Default[Authenticate/Challenge]Scheme

AutomaticAuthentication/Challenge were intended to only be set on one authentication scheme, but there was no good way to enforce this in 1.0. These have been removed as flags on the individual AuthenticationOptions, and have been moved into the base AuthenticationOptions which can be configured in the call to AddAuthentication(authenticationOptions => authenticationOptions.DefaultScheme = "Cookies").

There are now overloads that use the default schemes for each method in IAuthenticationService

"Windows" Authentication(HttpSys/IISIntegration)

The host behavior hasn't changed too much, but now they each register a single "Windows" authentication scheme. Also IISIntegration now conditionally registers the handler only if windows auth is enabled in IIS (if you have the latest version of ANCM, otherwise it's always registered as before).

Authorization changes

IAuthorizationService.AuthorizeAsync now returns AuthorizationResult instead of bool

In order to enable scenarios around authorization failures, IAuthorizationService now returns a result object which allows access to the reasons why AuthorizeAsync failed (either context.Fail(), or a list of failed requirements)

Removal of ChallengeBehavior => new PolicyEvaluator

In Auth 1.0, there was a ChallengeBehavior enum that was used to specify either Automatic/Unauthorized/Forbid behaviors to signal to the auth middleware what behavior the caller wanted. Automatic was the default and would go down the Forbid(403) code path if the middleware already had an authentication ticket, otherwise would result in Unauthorized(401).

In Auth 2.0, this behavior has been moved into a new Authorization.Policy package, which introduces the IPolicyEvaluator which uses both IAuthenticationService (when requested via policy.AuthenticationSchemes), and IAuthorizationService to decide whether to return a tri state PolicyAuthorizationResult (Succeeded/Challenged/Forbidden).

Overview of [Authorize]

The [Authorize] attribute hasn't changed much, but the there were some implementation details that have changed significantly in MVC's AuthorizeFilter, and here's an overview of how things work: AuthorizeFilter source

  1. An effective policy is computed by combining all of the requested policies/requirements from all relevant [Authorize] attributes on the controller/method/globally.
  2. IPolicyEvaluator.AuthenticateAsync(policy, httpContext) is called, by default, if the has specified any policy.AuthenticationSchemes, AuthenticateAsync will be called on each scheme, and each resulting ClaimsPrincipal will be merged together into a single ClaimsPrincipal set on context.User. If no schemes were specified, the evaluator will attempt to use context.User if it contains an authenticated user. This is usually the normal code path, as DefaultScheme/DefaultAuthenticateScheme will be set to the main application cookie, and the AuthenticationMiddleware will have already set context.User using this scheme's AuthenticateAsync() Authenticate logic
  3. If AllowAnoynmous was specified, authorization is skipped, and the filter logic short circuits and is done.
  4. Finally, IPolicyEvaluator.AuthenticateAsync(policy, authenticationResult, httpContext) is called with the result from step 2. This just basically turns into a call to IAuthorizationService.AuthorizeAsync, and the result is used to determine the appropriate Challenge/ForbidResult if needed.

Claims Transformation

Simpler, new IClaimsTransformation service with a single method: Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)

We call this on any successful AuthenticateAsync call.

        services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

        private class ClaimsTransformer : IClaimsTransformation {
            // Can consume services from DI as needed, including scoped DbContexts
            public ClaimsTransformer(IHttpContextAccessor httpAccessor) { }
            public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal p) {
                p.AddIdentity(new ClaimsIdentity());
                return Task.FromResult(p);
            }
        }

Known issues/breaking changes:

VahidN commented 7 years ago

@AndyCreigh You should change the Cookie.SecurePolicy: https://github.com/aspnet/Identity/issues/1364#issuecomment-323321248

davidfowl commented 7 years ago

@rbasniak you cut out a bunch of your code, the logs claim you have identity running. Show more of the code.

VahidN commented 7 years ago

@PaulAngelino

            // should be added after the  AddIdentity
            services.ConfigureApplicationCookie(identityOptionsCookies =>
            {
               identityOptionsCookies.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
               identityOptionsCookies.LoginPath = "/Account/Login";
               identityOptionsCookies.LogoutPath = "...";
            });
shakeri commented 7 years ago

@VahidN I have that same problem too.your code used cookie but i use jwt token. In .NET Core 1 i used following code:

            app.UseIdentity();
            app.UseCookieAuthentication(options:new CookieAuthenticationOptions()
            {
                AutomaticChallenge = false
            }); 

And it worked fine.but in .NET Core 2.0 i could not found this property.

VahidN commented 7 years ago

@shakeri It's documented here https://github.com/aspnet/Announcements/issues/232

rbasniak commented 7 years ago

@davidfowl I edited the original question to show the entire code of my Configure and ConfigureServices methods.

AndyCreigh commented 7 years ago

@VahidN Thanks for the suggestion, didn't work for me. I tried both options below, login is still redirecting to the same page when I have [Authorize] on the controller.

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.LoginPath = new PathString("/Account/Login/");
                options.LogoutPath = new PathString("/Account/Logoff/");
                options.AccessDeniedPath = new PathString("/Account/AccessDenied/");
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            });

       services.ConfigureApplicationCookie(identityOptionsCookies =>
        {
            identityOptionsCookies.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
        });.
davidfowl commented 7 years ago

@rbasniak calling AddIndentity changes the default authentication scheme. That's why cookies is being used instead of jwt.

shakeri commented 7 years ago

@rbasniak When you use jwt token authentication you don't use AddIndentity. In my Startup Class is the following:

        public void ConfigureServices(IServiceCollection services)
        {

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                       ....
                    };
                });

            services.AddAuthorization(options =>
            {
                options.AddPolicy("PolicyName", policy =>
                {
                    policy.RequireRole("Roles");
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireAuthenticatedUser();
                });
            });
             services.AddMvc(});
             services.AddScoped<IUserManager, UserManager>();
             services.AddScoped<UserManager<User>, UserManager>();
             services.AddScoped<IRoleManager, RoleManager>();
             services.AddScoped<RoleManager<Role>, RoleManager>();
             services.AddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
             services.AddScoped<ISignInManager, SignInManager>();
             services.AddScoped<SignInManager<User>, SignInManager>();

        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseAuthentication();
            app.UseMvc();
        }

And TokenController is the following:

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Post([FromForm]TokenViewModel model)
        {
            if (!ModelState.IsValid) return BadRequest(ModelState);
            var user = await _userManager.FindByNameAsync(model.Username);
            if (user == null) return BadRequest();
            if (!await _userManager.CheckPasswordAsync(user, model.Password)) return Unauthorized();
            var principal = await _signInManager.CreateUserPrincipalAsync(user);
            var token = _jwtService.GenerateClaimsIdentity(user,principal.Claims);
            var response = new
            {
                Displayname = user.Displayname,
                Username = user.Username,
                access_token = await _jwtService.GenerateEncodedToken(token.Claims),
                token_type = "bearer",
                expires_in = TotalSeconds
            };
            return new OkObjectResult(response);
        }

I never used AddIndentity.

davidfowl commented 7 years ago

@shakeri I was addressing @rbasniak

Tratcher commented 7 years ago

@rheid var identityToken = await HttpContext.GetTokenAsync("id_token"); looks correct, it's possible something else is failing or has changed. E.g. if you don't provide a scheme for GetTokenAsync then it uses the DefaultAuthenticateScheme or DefaultScheme. Are those configured differently in your new app?

rbasniak commented 7 years ago

@davidfowl @shakeri Thank you for the help, that solved the problem. Now my authenticated controllers are working again. The problem is now on the ClaimsTransformer. Before 2.0 it was working, now everytime the code hits the transformer the IsAuthenticathed property is false. Here's the code converted to 2.0:

public class ClaimsTransformer : IClaimsTransformation
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IHttpContextAccessor _httpContext;

    public ClaimsTransformer(UserManager<ApplicationUser> userManager, IHttpContextAccessor httpAccessor)
    {
        _userManager = userManager;
        _httpContext = httpAccessor;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (_httpContext.HttpContext.User.Identity.IsAuthenticated)
        {
            ...    
        }

        return principal;
    }
}

In 1.1 I added the transformer like this in my Configure() method:

app.UseClaimsTransformation(o => new ClaimsTransformer(userManager).TransformAsync(o));

Now in 2.0 as fas as I understood, it needs to be added as a service, so I'm doing like this:

services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

When I make a new request if I put a breakpoint in the ClaimsTransformer it this before getting into the controller, but here the user is not authenticated. After it hits the controller the user is authenticated correctly.

Is this correct?

PaulAngelino commented 7 years ago

@VahidN, are you saying that I should use services.ConfigureApplicationCookie instead of services.AddAuthentication().AddCookie(<CookieAuthenticationOptions>)?

geotinc commented 7 years ago

Can anyone help me to migrate to following code (1.1) to asp.net core 2.0 please

private void ConfigureAuth(IApplicationBuilder app)
        {
            SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_secretKey));

            app.UseSimpleTokenProvider(new TokenProviderOptions
            {
                Path = "/api/Token",
                Audience = "Mobile",
                Issuer = "Issuer",
                SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.XXXX),
                IdentityResolver = this.GetIdentity
            });

            TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                ValidateIssuer = true,
                ValidIssuer = "Issuer",

                ValidateAudience = true,
                ValidAudience = "Mobile",

                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };

            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                TokenValidationParameters = tokenValidationParameters
            });

        }

        private Task<ClaimsIdentity> GetIdentity(string username, string password)
        {
            if (username == "XXXXXX" && password == "XXXXXX")
            {
                return Task.FromResult(new ClaimsIdentity(new GenericIdentity(username, "Token"), new Claim[] { }));
            }
            return Task.FromResult<ClaimsIdentity>(null);
        }

Thans a lot, I try this without success:

private void ConfigureAuth(IApplicationBuilder app)
        {
            SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_secretKey));

            app.UseSimpleTokenProvider(new TokenProviderOptions
            {
                Path = "/api/Token",
                Audience = "Mobile",
                Issuer = "Issuer",
                SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.XXXX),
                IdentityResolver = this.GetIdentity
            });

        }

        private void ConfigureAuth(IServiceCollection services)
        {
            SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_secretKey));

            TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                ValidateIssuer = true,
                ValidIssuer = "Issuer",

                ValidateAudience = true,
                ValidAudience = "Mobile",

                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.RequireHttpsMetadata = false;
                options.Authority = "http://localhost:30940/"; // What I have to put here ??
                options.Audience = "resource-server"; // What I have to put here ??
                options.TokenValidationParameters = tokenValidationParameters;
            });
        }

        private Task<ClaimsIdentity> GetIdentity(string username, string password)
        {
            if (username == "XXXXXXX" && password == "XXXXXXXXX")
            {
                return Task.FromResult(new ClaimsIdentity(new GenericIdentity(username, "Token"), new Claim[] { }));
            }
            return Task.FromResult<ClaimsIdentity>(null);
        }

EDIT Found the problem: I've to add [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] And not just [Authorize] In the controller

Thanks a lot

irowbin commented 7 years ago

Anyone adding multiple schemes? In aspnet core 1xx i was able to add multiple cookie schemes. but after 2.0 i have no idea where to add these configs?

For example

services.AddAuthentication("myscheme1").AddCookie(o =>{
        o.ExpireTimeSpan = TimeSpan.FromHours(1);
        o.LoginPath = new PathString("/forUser");
        o.Cookie.Name = "token1";
        o.SlidingExpiration = true;
});

services.AddAuthentication("myscheme2").AddCookie(o =>{
        o.ExpireTimeSpan = TimeSpan.FromHours(1);
        o.LoginPath = new PathString("/forAdmin");
        o.Cookie.Name = "token2";
        o.SlidingExpiration = true;
});

and later in controller

// user controller
 await context.HttpContext.ChallengeAsync("myscheme1");

// admin controller
 await context.HttpContext.ChallengeAsync("myscheme2");
davidfowl commented 7 years ago

@irowbin The argument to AddAuthentication is setting the default scheme not adding a new one. This is what you want:

services.AddAuthentication()
    .AddCookie("myscheme1", o =>
    {
            o.ExpireTimeSpan = TimeSpan.FromHours(1);
            o.LoginPath = new PathString("/forUser");
            o.Cookie.Name = "token1";
            o.SlidingExpiration = true;
    })
    .AddCookie("myscheme2", o =>
    {
            o.ExpireTimeSpan = TimeSpan.FromHours(1);
            o.LoginPath = new PathString("/forAdmin");
            o.Cookie.Name = "token2";
            o.SlidingExpiration = true;
    });
irowbin commented 7 years ago

@davidfowl Thanks. Its working. πŸ‘

irowbin commented 7 years ago

hi @davidfowl My protected page keep redirecting to login page. I've followed instructions from above comments but no luck. I can see that the cookie is return in browser application tab but when i try to access claims from HttpContext.User.Claims , there is no results and redirected to login. What i am missing?

here is my code.

// startup.cs
services.AddAuthentication().AddCookie("scheme1", o =>
    {
            o.ExpireTimeSpan = TimeSpan.FromHours(1);
            o.LoginPath = new PathString("/account");
            o.Cookie.Name = "user_token";
            o.SlidingExpiration = true;
    });

// Configure method
 app.UseAuthentication();

// account controller signin method
     var claims = new List<Claim>
                {             
                 new Claim( UniqueName, UserName, ClaimValueTypes.String)
                };
     var identity = new ClaimsIdentity(claims);
     var principal = new ClaimsPrincipal(identity);
     await HttpContext.SignInAsync("scheme1", principal); 

After login success, i can see the cookie is return to the browser and also i can get cookies in context.HttpContext.Request.Cookies; but there is no claims in context.HttpContext.User.Claims. in aspnet core 1x i can access claims by doing this but not now. Thanks.

RehanSaeed commented 7 years ago

In 1.0, I was using middleware branching so I could support both basic and bearer authentication (Needed this while apps were migrated). I detected whether the basic or bearer authentication header was available and ran the relevant middleware like so:

application.UseIfElse(
    x => x.Request.Headers.HasBasicAuthenticationHeader(),
    x => x.UseBasicAuthentication(options => ...),
    x => x.UseJwtBearerAuthentication(options => ...))

In 2.0, there is only one middleware being run and I have to configure basic and bearer authentication for it. Is there a way I can add some branching logic instead of setting a default authentication scheme?

HesamKashefi commented 7 years ago

Hi , I've just migrated to .net core 2 and I see lot's of changes in Authentication setup, all good but I can't figure out how to migrate this problem : Back in the .Net Core 1 If we wanted to set a different pipeline for some requests we could use IApplicationBuilder Map Extension Method this way

 app.Map("/api", apiApp =>
            {
                apiApp.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
                {
                    Authority = "https://localhost:44380",
                    AutomaticAuthenticate = true,
                    ApiName = "AuthAPI"
                });
                apiApp.UseMvc();
            });

But now everything is gone up to the ConfigureServices Method, and I can't figure out how to migrate this part of my code! please help!

davidfowl commented 7 years ago

@irowbin looks like it should work. Can you make a minimal repro project using ASP.NET Core 2.0 that reproduces the problem in a github repository?

davidfowl commented 7 years ago

It turns out a massive amount of people are not using the AuthenticationSchemes property on the AuthorizeAttribute and instead are using middleware branching to select the current scheme for a request based on some logic (most commonly a path prefix /api).

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("/api/products")]
public class ProductsController : Controller
{
   [HttpGet]
   public List<Product> Get() => ...;
}

This will use the correct authentication scheme when doing auth for this specific controller.

As an alternative, here's a middleware that can be used to change the current user based on the authentication scheme.

public class AuthenticateSchemeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _scheme;

    public AuthenticateSchemeMiddleware(RequestDelegate next, string scheme)
    {
        _next = next;
        _scheme = scheme ?? throw new ArgumentNullException(nameof(scheme));
    }

    public async Task Invoke(HttpContext httpContext)
    {
        var result = await httpContext.AuthenticateAsync(_scheme);

        if (result.Succeeded)
        {
            httpContext.User = result.Principal;
        }

        await _next(httpContext);
    }
}

public static class AuthenticateMiddlewareExtensions
{
    public static IApplicationBuilder UseAuthenticationScheme(this IApplicationBuilder builder, string scheme)
    {
        return builder.UseMiddleware<AuthenticateSchemeMiddleware>(scheme);
    }
}

Usage looks like this:

In Startup.ConfigureServices

void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication()
                  .AddJwtBearer(...);
}

In Startup.Configure

app.Map("/api", sub => sub.UseAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme));
[Authorize]
[Route("/api/products")]
public class ProductsController : Controller
{
   [HttpGet]
   public List<Product> Get() => ...;
}

UPDATE: I should point out that if you're using this middleware and the user isn't authorized, the challenge/forbid issued will end up being the default scheme. If none is set, an exception will be thrown.

irowbin commented 7 years ago

@davidfowl I've added a repo. Sorry for the boilerplate stuffs. πŸ˜† The startup.cs and the HomeController.cs.

The claims is empty.

HesamKashefi commented 7 years ago

@davidfowl Thank you so much! I just needed that Authentication for my Api Controllers and I thought using AuthenticationScheme and adding that Authentication MiddleWare may have impact on the performance of Paths that don't start with "api" , isn't this true?

Anyway, I just followed tutorials on the Internet , sorry for not using AuthenticationScheme on AuthorizeAttribute 😁

kiss96803 commented 7 years ago

@rbasniak Can you show your change code in Startup.cs ?

poplawsm commented 7 years ago

Is there any built-in solution to use Claims Transformation when using Windows Authentication on IIS or do I have to create ClaimsTransformerMiddleware myself?

rbasniak commented 7 years ago

@kiss96803 My entire Configure and ConfigureServices are posted a few comments above, because I was having another problem converting my authentication code do 2.0. Here's the link to my code: https://github.com/aspnet/Security/issues/1310#issuecomment-323560279

irowbin commented 7 years ago

@davidfowl After some struggle i am able to get claims by doing like this.

// login method
public async Task<IActionResult> Login()
{
     var claims = new List<Claim>
                {             
                 new Claim( UniqueName, UserName, ClaimValueTypes.String)
                };

     var identity = new ClaimsIdentity(claims);
     var principal = new ClaimsPrincipal(identity,"scheme1"); // notice authentication type. if i remove it, won't work.
     await HttpContext.SignInAsync("scheme1", principal); 
     return Ok();
}

//  only works if i decorate my controller like this
 [Authorize(AuthenticationSchemes ="scheme1")] // if i remove it, claims are empty.
 public class HomeController : Controller
 {
         public IActionResult Index()
        {
           var claims = HttpContext.User.Claims; // claims are available.
            return Ok();
        }
 }

In aspnet core 1x i don't have to add that decorator to the controller and the authentication type to the ClaimsIdentity to get claims but now it won't work without it. The aspnet core docs is really poor with minimal details.

cgountanis commented 7 years ago

This is how it works for me currently, maybe this will help someone with custom roles. In the controller code you can specify the roles dynamically then in the controller you can specify the roles in the authorization annotation e.g. [AllowAnonymous] or [Authorize(CustomRoles.User)], etc. I have a class called CustomRoles (sudo Enum basically) but that can easily be just string values in the example where used.

ConfigureServices:

// Only needed for custom roles.
services.AddAuthorization(options =>
            {
                options.AddPolicy(CustomRoles.Administrator, policy => policy.RequireRole(CustomRoles.Administrator));
                options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                options.AddPolicy("UserId", policy => policy.RequireClaim("UserId"));
                options.AddPolicy("Email", policy => policy.RequireClaim("Email"));
            });
// Needed for cookie auth.
services
    .AddAuthentication(o =>
    {
        o.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddCookie(options =>
    {
        options.LoginPath = "/Authorization/Login";
        options.LogoutPath = "/Authorization/Logout";
        options.AccessDeniedPath = new PathString("/Home/Forbidden/");
        options.Cookie.Name = "cookie";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.SlidingExpiration = true;
    });

Configure: app.UseAuthentication();

Controller:

// Create cookie for this site.
var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()),
    new Claim(ClaimTypes.Name, user.Email),
    new Claim(ClaimTypes.Email, user.Email),
    new Claim(ClaimTypes.GivenName, user.FirstName),
    new Claim(ClaimTypes.Surname, user.LastName),
    new Claim(ClaimTypes.Role, CustomRoles.Administrator)
};
var claimsIdentity = new ClaimsIdentity(claims, "Login");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

// Sign in with specific auth properties.
await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    claimsPrincipal,
    new AuthenticationProperties
    {
        IssuedUtc = DateTime.UtcNow,
        ExpiresUtc = DateTime.UtcNow.AddMinutes(_settings.LoginCookieExpirationMinutes),
        AllowRefresh = false,
        IsPersistent = false
    }
);
HaoK commented 7 years ago

Since this is an older closed issue, please use https://github.com/aspnet/Security/issues/1338 so we have one consolidated issue

Tratcher commented 7 years ago

@rbasniak why are you accessing HttpContextAccessor.HttpContext.User in your transformation rather than operating on the ClaimsPrincipal passed to you as an argument?

Transformations are run prior to assigning HttpContext.User:

https://github.com/aspnet/HttpAbstractions/blob/27b0f60f09f5b8a91a1d0706c3a2ec94ee00eee9/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs#L71

https://github.com/aspnet/Security/blob/5b29bced0d2f1cfc78843bcd9ec9a828c6fb5fef/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs#L57

Tratcher commented 7 years ago

@irowbin if you specify services.AddAuthentication("scheme1") then that's the scheme that will be used for the default [Authorize], otherwise you have to specify it on each authorize attribute.

Tratcher commented 7 years ago

@RehanSaeed UseJwtBearer already no-ops if the header doesn't contain Bearer, and I assume UseBasicAuth would do something similar. The far more interesting question I would expect is how to specify which to issue a challenge for.

Tratcher commented 7 years ago

@poplawsm implement a transformer: https://github.com/aspnet/HttpSysServer/issues/389

RehanSaeed commented 7 years ago

@Tratcher I'm thinking I could override the AuthenticationSchemeProvider's GetDefaultAuthenticateSchemeAsync() method with my own implementation that selects the authentication scheme based on the HTTP headers in the request, instead of using the DefaultAuthenticateScheme. Alternatively I can also override this behaviour in the AuthenticationMiddleware itself. Would that be sufficient for my needs?

Tratcher commented 7 years ago

@RehanSaeed Sure, try it. However, how do you decide which handler should handle the challenge for unauthenticated requests?

Tratcher commented 7 years ago

@AndyCreigh are you mixing http and https? Chrome doesn't like this. https://github.com/aspnet/Mvc/issues/6673

rbasniak commented 7 years ago

@Tratcher Thank you, the parameter type changed and I didn't notive that it has the authenticaded user data. Now the whole authentication system is back...

kazantsev-nikita commented 7 years ago

I'm using method GetAuthenticationSchemes to get authentification schemes: var schemes = _httpContextAccessor.HttpContext.Authentication.GetAuthenticationSchemes(); But after migrating project to .Net Core 2.0 every build I see that 'HttpContext.Authentication' is obsolete. Is it possible to get authentification schemes with?

HaoK commented 7 years ago

Use IAuthenticationSchemeProvider.GetAllSchemesAsync to get them all

https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.AspNetCore.Authentication.Abstractions/IAuthenticationSchemeProvider.cs#L19

You'll have to get the service from DI/request services, its not hanging off of context directly anymore

coderabsolute commented 7 years ago

This thread is complex, can you please provide a simple JWT Token Bearer example for Core 2 please?

chikien276 commented 7 years ago

@coderabsolute

 services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(o =>
            {
                o.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(Configuration["JWT:SignKey"])),

                    ValidateAudience = true,
                    ValidAudience = Configuration["JWT:ValidIssuer"],

                    ValidateIssuer = true,
                    ValidIssuer = Configuration["JWT:ValidIssuer"],

                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });
irowbin commented 7 years ago

@Tratcher Always redirecting to scheme1 why?

services.AddAuthentication()
    .AddCookie("scheme1", o =>
    {
            o.ExpireTimeSpan = TimeSpan.FromHours(1);
            o.LoginPath = new PathString("/forUser");
            o.Cookie.Name = "token1";
            o.SlidingExpiration = true;
    })
    .AddCookie("scheme2", o =>
    {
            o.ExpireTimeSpan = TimeSpan.FromHours(1);
            o.LoginPath = new PathString("/forAdmin");
            o.Cookie.Name = "token2";
            o.SlidingExpiration = true;
    });

 [Authorize(AuthenticationSchemes ="scheme1,scheme2")] 
 public class HomeController : Controller
 {
         public IActionResult Index()
        {          
            return Ok("cool");
        }
 }
davidfowl commented 7 years ago

@irowbin What do you expect it to do?

irowbin commented 7 years ago

Hi @davidfowl I am filtering claims from method.

 Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next )

I want to invoke this method but the authentication keep redirecting me to the login path without executing this method? In 1x i can get claims without that authorize decorator, now the things are changed. I have multiple login pages based on that schemes so i am redirecting manually by getting schemes from method like this

[ClaimRequirement("home", "read", "scheme1")]      
public IActionResult Index1()
{
      return View();
}

[ClaimRequirement("home", "read", "scheme2")]      
public IActionResult Index2()
{
      return View();
}

public class ClaimRequirementFilter : IAsyncActionFilter
{

  private readonly string _scheme;
  public ClaimRequirementFilter (string scheme)
  {
    _scheme = scheme;
  }

  Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next )
  {   
      await context.HttpContext.ChallengeAsync( _scheme );
  }

}
davidfowl commented 7 years ago

In 1x i can get claims without that authorize decorator, now the things are change

You just want to get the user for the appropriate scheme?

var result = await context.AuthenticateAsync(scheme);

How were you getting at the right user before?

davidfowl commented 7 years ago

@irowbin I think I know what you were doing before (through the series of posts), please correct me if I'm wrong. You had 2 cookie middleware in the pipeline with automatic authenticate set to true. When the request came in, you had potentially multiple identities on the claims principal and your custom action filter would get the user for the appropriate scheme (I'd like to see that code) and check the claims against them.

Is that right?

irowbin commented 7 years ago

YES that's right. I've already migrate to 2.0. this is how i try to get claims

 Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next )
  {   
      var anyClaims =  context.HttpContext.User.Claims.Any( c => c.Type == JwtRegisteredClaimNames.UniqueName );

    if(!anyClaims )
        await context.HttpContext.ChallengeAsync( _scheme );
    esle next();

  }

If i remove that authorize attribute then there is no claims and users but if i add that, the users are available but then i have to explicit the schemes too.

davidfowl commented 7 years ago

If i remove that authorize attribute then there is no claims and users but if i add that, the users are available but then i have to explicit the schemes too.

Without writing manual code there's no way to merge all the identities into the single claims principal. There's a helper but I'd recommend against it.

What you want to do doesn't require a filter at all, is there any reason you wrote one instead of using an authorization policy?

From this attribute - [ClaimRequirement("home", "read", "scheme1")]

What does "home" and "read" map to?

For completeness though, the code you would write looks like this:

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
    var result = await context.HttpContext.AuthenticateAsync(_scheme);
    if (result.Succeeded)
    {
        var anyClaims = result.Principal.Claims.Any(c => c.Type == JwtRegisteredClaimNames.UniqueName);

        if (!anyClaims)
        {
            await context.HttpContext.ChallengeAsync(_scheme);
            return;
        }
    }

    await next();
}
irowbin commented 7 years ago

@davidfowl My custom action filter is to look for the permission from database. create, read, update, delete so i have to invoke custom action filter.

[ClaimRequirement(
"home", // the protected resource. Such as the controller name.
"read",  //  readonly permission
"scheme1")] // authentication type
public async Task<IActionResult> Index()
{
      return View();
}