ASP.NET Core 2 MVC Cookie based authentication. Are there more options? - authentication

I have ASP.NET Core 2 MVC project with Cookie based authentication. Code I have attached below. Right now I don't have any problems but I am looking to improve it or any other options that I also need to explore. More Secure. I really need direction.
Removed some unnecessary code.
Question:
1 - Am I doing it right. Anything to add more?
2 - Are there more options for authentication. More secure?
After Successfull Credentials
var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false);
if (result.Succeeded)
{
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
identity.AddClaim(new Claim(ClaimTypes.Email, model.Email));
identity.AddClaim(new Claim(ClaimTypes.Name, model.Email));
identity.AddClaim(new Claim("Admin", "Admin"));
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
// Authenticate using the identity
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = false });
if (Type == "Admin")
return RedirectToAction(nameof(AdminController.Index), "Admin");
return RedirectToAction(nameof(AccountController.Index), "Account");
}
services.AddAuthentication(options =>
{
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie();
services.AddAuthorization(options =>
{
options.AddPolicy("admin-policy", x => { x.RequireClaim("Admin"); });
});
On Controller
[Authorize]
[Authorize(Policy = "admin-policy")]

When it comes to a website-style application, no. Cookie auth is really the best you've got. HTTP requests are idempotent. To access a protected endpoint, the client (in this case, a web browser) must send something along with the request that either serves to authenticate or simply identify the client has having already been authenticated. That's where the cookie comes in. There's other ways this can be done, but principally, browsers don't really support those workflows.
With a different client, particularly clients where you have control over how the request is formatted, JWTs would probably be your best bet. However, again, that doesn't help you with a web browser. It does mean, though, that can go cookieless for things like AJAX or HttpClient requests, though, since, again, you can control the headers there.

Related

Blazor Server cookie validation

I am building a Blazor Server application, which authenticates to AD and issues a cookie used for authentication and authorization (few AD claims are stored in the cookie).
Is there anything "special" I need to add to ensure the cookie was not tampered?
I've already added this part to handle cookie expiration.
services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.Cookie.MaxAge = options.ExpireTimeSpan;
options.SlidingExpiration = true;
});
When a user logs in, we are building a ClaimsPrincipal which adds some AD groups as claims:
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new()
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(15),
RedirectUri = Request.Host.Value
});
For example, some users are getting special privileges based on a claim stored in the cookie, we need to validate if the cookie was tampered or not.

ASP.NET Core 3.1 Use both OpenIDConnect and Custom Cookie Authentication

I have an existing application that makes use of Cookie Authentication, and would like to add the ability to authenticate users using Active Directory. The current application uses Cookie based authentication and custom authorisation - roles in a database.
I am adding bits from example located here:
Add sign-in with Microsoft to an ASP.NET Core web app
When I run the application I get an error:
System.InvalidOperationException: Scheme already exists: Cookies
What is the correct way to configure OpenIdConnect and Cookie Authentication.
// STEP 1 Basic Cookie Auth
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth";
options.AccessDeniedPath = "/Home/AccessDenied";
options.Cookie.IsEssential = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(day/2.0);
options.Cookie.HttpOnly = true; // not accessible via JavaScript
options.Cookie.Name = "login_token";
options.TicketDataFormat = new CustomJwtDataFormat(
SecurityAlgorithms.HmacSha256,
tokenValidationParameters);
});
// STEP 2 OpenID Connect Auth
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "Cookies", true);
I am not able to find any examples using both Cookie Authentication and OpenID Connect. Is this possible? Allowing users to login selectively using Active Directory authentication, or local authentication (details stored in local database).
After changing the "Cookie" name, get's rid of the error message,
but breaks the local authorisation, e.g.
When a valid Username and Password is given, I typically
authorise the login.
HttpContext.Response.Cookies.Append("login_token", token, GetCookieOptions());
Currently with OpenIDConnect configured User.Identity.IsAuthenticated
remains false.
According to the error messages, it tell you that you have multiple Scheme which named cookies.
According to the AddMicrosoftIdentityWebApp Method document, you could find the third parameter name is the cookieScheme.
The cookie-based scheme name to be used. By default it uses "Cookies".
But you have already set this name at above, so you should use other one. For example: "ADCookies".
Like below:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "ADCookies", true);
The mixed approach is a minefield but the below is allowing use to Authenticate Users via IdentityServer4 using OIDC while authenticating the Application into AzureAD with Identity.Web to get tokens for Api calls.
services.AddAuthentication(options =>
{
options.DefaultScheme = "IS4Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("IS4Cookies")
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = "IS4Cookies";
// Get IdentityServer configuration from appsettings.json.
var config = Configuration.GetSection("IdentityServerOptions").Get<IdentityServerOptions>();
options.Authority = config.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = config.ClientId;
options.ClientSecret = config.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role");
options.ClaimActions.MapJsonKey("role", System.Security.Claims.ClaimTypes.Role);
options.ClaimActions.MapJsonKey("email", "email");
options.ClaimActions.MapJsonKey("preferred_username", "preferred_username");
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.Response.Redirect("/");
context.HandleResponse();
return Task.FromResult(0);
}
};
})
.AddMicrosoftIdentityWebApp(Configuration, "AzureOptions")
.EnableTokenAcquisitionToCallDownstreamApi(new string[]{"sms.all" })
.AddInMemoryTokenCaches();
This is what I use and it works, you just need to specify the configureCookieAuthenticationOptions and set the name inside there and you should be good to go, also I had to use lax for SameSite or it would not work for me.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(identityOptions =>
/* {identityOptions.ClientId ="";}, // if you want to specify the options manually instead of Configuration.GetSection() call*/
Configuration.GetSection("AzureAd"),
configureCookieAuthenticationOptions: authCookie => { // Setup SSO cookie
authCookie.Cookie.Name ="Your.Cookie.Name.Here";// change name to hide .net identifiers in name
authCookie.Cookie.HttpOnly = true;// make so client cannot alter cookie
authCookie.Cookie.SecurePolicy = CookieSecurePolicy.Always;// require https
authCookie.Cookie.SameSite = SameSiteMode.Lax;// from external resource
// verify options are valid or throw exception
authCookie.Validate();
}
);
You may or may not need all of the authCookie values here, but it should get you started in the right direction!
It's possible to mix two mechanisms.
I use MicrosoftIdentity authentication for access to administration web pages and cookies authentication for my APIs and SignalR hubs.
I use this in startup ConfigureServices
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("CookiesApiScheme", options =>
{
options.SlidingExpiration = true;
// There is no redirection to a login page for APIs and SignalR Hubs, I just made a call to /Api/Login/SignIn with credential
options.AccessDeniedPath = new PathString("/Api/Login/AccessDenied"); // Action who just returns an Unauthorized
})
.AddMicrosoftIdentityWebApp(Configuration); // By default scheme is "CookieAuthenticationDefaults.AuthenticationScheme"
And in API controller you can use something like this
[Route("api/[controller]")]
[ApiController]
[Authorize(Roles = Roles.ADMIN)]
[Authorize(AuthenticationSchemes = "CookiesApiScheme")]
public class DefaultController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Based on this post: ASP.NET Core 2.0 AzureAD Authentication not working

Using OpenIdConnect and JwtBearer authentication together

My ASP.NET Core 2.2 web site uses OpenIdConnect to connect to an external OIDC provider for authentication. So we get a redirect to the provider, you log in, it returns back to the site. All of this is handled server side and works great.
Our web site uses javascript to communicate to the API backend, which is decorated with an [Authorize] attribute.
Now, we want to share our API with another application. I am having trouble understanding how to make it work with the existing OpenIdConnect provider. If I use Postman to make a call, I add a Bearer token but I am returned the log in screen for my OIDC provider. I understand this, it's set up for OIDC.
I have tried using AddJwtBearer with [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] set on my Controller and when I do that I can successfully pass in a Bearer token and call my API from Postman but I can't make a call from a javascript page on my site.
So the ultimate question is, how do I configure this site so that I can call an API from my site itself with OIDC and also call from an external app using a Bearer token?
The code looks like this:
public void ConfigureServices(IServiceCollection services)
{
var authbuilder = services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
authbuilder.AddCookie(p =>
{
p.SlidingExpiration = true;
p.Events.OnSigningIn = (context) =>
{
context.CookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(14);
return Task.CompletedTask;
};
});
authbuilder.AddOpenIdConnect(options =>
{
options.Authority = Configuration["OpenIdConnectSettings:AuthorityUrl"];
options.ClientSecret = Configuration["OpenIdConnectSettings:ClientSecret"];
options.ClientId = Configuration["OpenIdConnectSettings:ClientId"];
}
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.GetClaimsFromUserInfoEndpoint = true;
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
// validation logic omitted
}
EDIT: Any external apps will also use the same OIDC provider. It's just a matter of getting my API Controllers to work from the web site itself and being called from another app that uses the same OIDC provider.
EDIT: I think I may have gotten it to work by adding this code:
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
if the other application is a service/application (not a human facing client) then that application needs to register as a client in your OIDC provider and then preferably use the client credentials flow, to get a token from your OIDC provider and pass it to the API.
The API only trusts access tokens from your OIDC provider (the token issuer), so the application needs to authenticate against the app and to get a proper access token.

aspnet core 2.2 External Authentication

Created an Authentication Api to handle the auth for several apps. This is a basic auth. username and pw. No OAuth with Google etc. The api gets called with the credentials and it responds with an AthenticationResult. It works correctly except on AuthenticationResult.Success. As I learned I cannot serialize the ClaimsPrincipal. As I am reading it seems the answer it to convert to a token. Is this correct? The AuthenticationResult.Failed serializes w/o issue. What is the best solution here. I will continue to look.
thx for reading
General Steps
That's correct, you'll need to complete the following steps:
Return a token from your authentication API.
Configure your application for JWT Bearer authentication.
Include that token as part of an authorize header on every request to the server.
Require authentication/authorization in your controllers.
There is an excellent ASP.NET Core 2.2 JWT Authentication Tutorial you should check out.
There's too much code involved to post all of it in it's entirety, but here are some key snippets (some code slightly modified for greater clarity out of context from the tutorial):
Some Key Code Snippets
Creating the token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
// 'user' is the model for the authenticated user
// also note that you can include many claims here
// but keep in mind that if the token causes the
// request headers to be too large, some servers
// such as IIS may reject the request.
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
Configuring JWT Authentication (in Startup.cs ConfigureServices method)
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Don't forget to configure the app to actually use authentication in Startup.cs Configure method:
app.UseAuthentication();

Where to store JWT Token in .net core web api?

I am using web api for accessing data and I want to authenticate and authorize web api.For that I am using JWT token authentication. But I have no idea where should I store access tokens?
What I want to do?
1)After login store the token
2)if user want to access any method of web api, check the token is valid for this user,if valid then give access.
I know two ways
1)using cookies
2)sql server database
which one is the better way to store tokens from above?
Alternatively, if you just wanted to authenticate using JWT the implementation would be slightly different
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var user = context.Principal.Identity.Name;
//Grab the http context user and validate the things you need to
//if you are not satisfied with the validation fail the request using the below commented code
//context.Fail("Unauthorized");
//otherwise succeed the request
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey("MyVeryStrongKeyHiddenFromAnyone"),
ValidateIssuer = false,
ValidateAudience = false
};
});
still applying use authentication before use MVC.
[Please note these are very simplified examples and you may need to tighten your security more and implement best practices such as using strong keys, loading configs perhaps from the environment etc]
Then the actual authentication action, say perhaps in AuthenticationController would be something like
[Route("api/[controller]")]
[Authorize]
public class AuthenticationController : Controller
{
[HttpPost("authenticate")]
[AllowAnonymous]
public async Task<IActionResult> AuthenticateAsync([FromBody]LoginRequest loginRequest)
{
//LoginRequest may have any number of fields expected .i.e. username and password
//validate user credentials and if they fail return
//return Unauthorized();
var claimsIdentity = new ClaimsIdentity(new Claim[]
{
//add relevant user claims if any
}, "Cookies");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Request.HttpContext.SignInAsync("Cookies", claimsPrincipal);
return Ok();
}
}
in this instance I'm using cookies so I'm returning an HTTP result with Set Cookie. If I was using JWT, I'd return something like
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]LoginRequest loginRequest)
{
//validate user credentials and if they validation failed return a similar response to below
//return NotFound();
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("MySecurelyInjectedAsymKey");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
//add my users claims etc
}),
Expires = DateTime.UtcNow.AddDays(1),//configure your token lifespan and needed
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey("MyVerySecureSecreteKey"), SecurityAlgorithms.HmacSha256Signature),
Issuer = "YourOrganizationOrUniqueKey",
IssuedAt = DateTime.UtcNow
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTimeOffset.UtcNow.AddHours(4);//you can set this to a suitable timeframe for your situation
cookieOptions.Domain = Request.Host.Value;
cookieOptions.Path = "/";
Response.Cookies.Append("jwt", tokenString, cookieOptions);
return Ok();
}
I'm not familiar with storing your users tokens on your back end app, I'll quickly check how does that work however if you are using dotnet core to authenticate with either cookies or with jwt, from my understanding and experience you need not store anything on your side.
If you are using cookies then you just need to to configure middleware to validate the validity of a cookie if it comes present in the users / consumer's headers and if not available or has expired or can't resolve it, you simply reject the request and the user won't even hit any of your protected Controllers and actions. Here's a very simplified approach with cookies.(I'm still in Development with it and haven't tested in production but it works perfectly fine locally for now using JS client and Postman)
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "yourCookieName";
options.Cookie.SameSite = SameSiteMode.None;//its recommended but you can set it to any of the other 3 depending on your reqirements
options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents
{
OnRedirectToLogin = redirectContext =>//this will be called if an unauthorized connection comes and you can do something similar to this or more
{
redirectContext.HttpContext.Response.StatusCode = 401;
return Task.CompletedTask;
},
OnValidatePrincipal = context => //if a call comes with a valid cookie, you can use this to do validations. in there you have access to the request and http context so you should have enough to work with
{
var userPrincipal = context.Principal;//I'm not doing anything with this right now but I could for instance validate if the user has the right privileges like claims etc
return Task.CompletedTask;
}
};
});
Obviously this would be placed or called in the ConfigureServices method of your startup to register authentication
and then in your Configure method of your Startup, you'd hookup Authentication like
app.UseAuthentication();
before
app.UseMvc()