ASP.NET MVC claims-based users with "external" authentication - c#

Background
Our web applications use external authentication, in a sense that users' usernames/passwords aren't validated locally, but are being validated "outside" the web app in a central, single-sign-on type website. The authentication proof (and user's identification) becomes available via local server variables (HTTP_EMPLOYEEID, etc.). However, it's not quite external like authenticating against Google, Facebook, or another OAuth based setup. So I just wanted to make that distinction, so it doesn't collide with terms "External Logins" in ASP.NET Identity / Owin.
Problem
I'm trying to figure out a clean way to leverage the authenticated user data (from server variables) and pass it over to ASP.NET authentication. However, the user profile and role data has to be looked up against a web service before the user can be logged into the app.
I want to use Owin and Claims-based identity, but I am not sure if I should also use ASP.NET Identity, as well, or just do a more "pure" implementation with claims. I like the idea of not reinventing the wheel, but I also don't want to force a square peg into a round hole (as the saying goes), if the way the user is identified and looked up from a web service does not fit a typical ASP.NET Identity usage.
For example, if I take a more purist approach, I could do something like:
// Get the current user's id
var userId = HttpContext.Current.Request.ServerVariables["HTTP_EMPLOYEEID"];
// Get profile and role data from a web service
MyUser user = MyUserService.GetUserById(userId);
// Create claims
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, user.Id),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role), // there can be more roles, but you get the idea
// etc.
};
// Establish identity and login
var identity = new ClaimsIdentity(claims, "CookieAuthentication");
HttpContext.Current.GetOwinContext().Authentication.SignIn(identity);
But I also know I could be using ASP.NET Identity (just without Entity Framework stuff), and just implement IUser, IUserStore, IRoleStore (and whatever else is minimally required), and use an existing, established framework from Microsoft for handling this. The argument would be that this is more in line with current standards, and could potentially be extended easier for other types of authentication (if, say, a local username/password, or Google/Facebook become other allowed authentication options eventually, in addition to the current, ServerVariables-based setup).
Any advice from people who've been down this path before? Should I treat the server variable injected data as custom middleware and leverage it via ASP.NET Identity, or just not worry about where to fit it in that world, and go in a more of a "purist" approach as described above?
p.s. I'm using ASP.NET 4.6.1, and not the new ASP.NET Core.

I have similar saturation. I do not want to use entire ASP.Net Identity, because I need to authenticate user again our external authentication.
So I just use OWIN Claim Authentication which basically creates Authentication cookie with Claims; Similar to Form Authentication we used in the old days.
public class OwinAuthenticationService
{
private readonly HttpContextBase _context;
private const string AuthenticationType = "ApplicationCookie";
public OwinAuthenticationService(HttpContextBase context)
{
_context = context;
}
public void SignIn(User user)
{
IList<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
};
foreach (Role role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignIn(identity);
}
public void SignOut()
{
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignOut(AuthenticationType);
}
}
Startup.cs
Note: I have Angular using both MVC and Web API, so I return 404 message for REST instead of 404 Page.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsApiRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
}
private static bool IsApiRequest(IOwinRequest request)
{
string apiPath = VirtualPathUtility.ToAbsolute("~/api/");
return request.Uri.LocalPath.ToLower().StartsWith(apiPath);
}
}

Related

How to grant access to users externally authenticated and not registered in my WebApp using asp.net core 2.2?

Summary
This is my first try with OAuth2 and External Login Mechanisms.
I'm creating a WebApp that will expose API features through a user-friendly UI.
In order to make API calls, I need to receive an access token from QBO that grants access to resources.
So, my WebApp has an external login option which I use to authenticate against QBO, and then authorize my app.
Everything works fine until...
Services Configuration
Based on a tutorial for GitHub authentication, I came up with this.
services.AddAuthentication(o => {
o.DefaultAuthenticateScheme = IdentityConstants.ExternalScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
o.DefaultChallengeScheme = IdentityConstants.ExternalScheme;
})
.AddOAuth("qbo", "qbo", o => {
o.CallbackPath = new PathString("/signin-qbo");
o.ClientId = Configuration["ecm.qbo.client-id"];
o.ClientSecret = Configuration["ecm.qbo.client-secret"];
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("com.intuit.quickbooks.accounting");
o.AuthorizationEndpoint = Configuration["ecm.qbo.authorization-endpoint"];
o.TokenEndpoint = Configuration["ecm.qbo.token-endpoint"];
o.UserInformationEndpoint = Configuration["ecm.qbo.user-info-endpoint"];
o.Events.OnCreatingTicket = async context => {
var companyId = context.Request.Query["realmid"].FirstOrDefault() ?? throw new ArgumentNullException("realmId");
var accessToken = context.AccessToken;
var refreshToken = context.RefreshToken;
Configuration["ecm.qbo.access-token"] = accessToken;
Configuration["ecm.qbo.refresh-token"] = refreshToken;
Configuration["ecm.qbo.realm-id"] = companyId;
context.Backchannel.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
context.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await context.Backchannel.GetStringAsync(context.Options.UserInformationEndpoint);
var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
var user = (ClaimsIdentity)context.Principal.Identity;
user.AddClaims(new Claim[] {
new Claim("access_token", accessToken),
new Claim("refresh_token", refreshToken),
new Claim(ClaimTypes.GivenName, result["givenName"]),
new Claim(ClaimTypes.Surname, result["familyName"]),
new Claim(ClaimTypes.Email, result["email"]),
new Claim(ClaimTypes.Name, result["givenName"]+" "+result["familyName"])
});
};
});
This works. I can add my claims based on user information, the context.Principal.Identity indicates that it's authenticated.
For some reasons, it seems to try and redirect to `/Identity/Account/Login?returnUrl=%2F. Why is that?
Login page redirection
Here, I don't get why I get the redirection and this confuses me a lot. So, I added the AccountController just to try and shut it up.
namespace ecm.backoffice.Controllers {
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller {
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string returnUrl = "/") {
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout(string returnUrl = "/") {
await Request.HttpContext.SignOutAsync("qbo");
return Redirect(returnUrl);
}
}
}
And this creates more confusion than it solves, actually. I'm lost here...
Apply Migrations
This Individual User Authentication WebApp seems to use Identity which looks like it creates a lot of behind the scene mechanisms. I first tried to register to my app, and had to "Apply Migrations", which I totally get, since the data model wasn't initialized.
So, I clicked the Apply Migrations button. I though I was okay with this...
Entity Framework Core
I am aware that the app is using Entity Framework Core for its persistence mechanism, hence the registration process, etc. And to configure it, I needed to add these lines to the services configs.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
It looks like it just won't work at all.
Thoughts
At this point, I think that the message says it Entity Framework just can't load the user information from its underlying data store. I totally understand that, and I don't want to register this user. I just want to take for granted that if QBO authenticated the user, it's fine by me and I grant open bar access to the WebApp features, even if the user ain't registered.
How to tell that to my WebApp?
Related Q/A I read prior to ask
Prevent redirect to /Account/Login in asp.net core 2.2
ASP.NET Core (2.1) Web API: Identity and external login provider
asp.net core 2.2 redirects to login after successful sign in
External Login Authentication in Asp.net core 2.1
And many more...

How to pull user roles from database when wiring up OAUTH for ASP.NET Identity 2.0?

I'm working on some Web APIs, and have been tasked with adding role based authorization to some of the endpoints using ASP.NET Identity 2.0.
I have created an API based administration structure for managing users and roles, and have hit a sticking point when attempting to implement authorization/authentication using OAUTH Bearer tokens.
(NOTE I read that JWT is better to use and makes supplying user data simpler, but the ask is for vanilla OAUTH)
So as for the code here is what I have so far, including the sticking point:
Startup.cs:
private static void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
// Configure the db context, user manager and role manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
// Create the options for the token authorization
var oauthAuthorizationServerOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(1),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(oauthAuthorizationServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
SimpleAuthorizationServerProvider.cs:
public sealed class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// We are not validating clients at this point, simply "resource owners" i.e. user / pass combos
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
using (var repo = new AuthorizationRepository())
{
var user = repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
// How do you pull the user data (including roles) from the database here, and place into a claim for API consumption??
}
}
What I've found online is the following, but this only creates a default role for the user (or list of roles):
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim(ClaimTypes.Role, "admin"));
identity.AddClaim(new Claim("sub", context.UserName));
context.Validated(identity);
The above code is a problem because it will validate the user, but then assign EVERY user the role of admin in the generated token!
Any help would be appreciated and thank you for your help!
The issue is that you are not setting the claims up from the ApplicationUserManager, which can do a lot of the heavy lifting for you. Further, you're just setting up a generic ClaimsIdentity which, as you already pointed out, will always return the same set of roles for all users.
In GrantResourceOwnerCredentials(), what you want to do is:
//
// Get an instance of the ApplicationUserManager that you've already registered
// with OWIN
//
var mgr = context.OwinContext.GetUserManager<ApplicationUserManager>();
//
// Have the ApplicationUserManager build your ClaimsIdentity instead
//
var identity = await mgr.CreateIdentityAsync(user,
context.Options.AuthenticationType);
//
// Then here, you could add other application-specific claims if you wanted to.

ASP.NET Core 2.1 How to determine Claims without Authorize Attribute?

I'm building a web application which uses the cookie authentication built into ASP.NET Core 2.1.
I have my own sign in method which queries my own custom password verification and setting of claims. Roughly it looks like this:
public async Task<ActionResult<LoginResponse>> DoLogin([FromBody] LoginRequest req)
{
// fetch account and verify password
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, account.AccountId.ToString(), ClaimValueTypes.Integer),
new Claim(ClaimTypes.Email, account.EmailAddress, ClaimValueTypes.Email),
new Claim(ClaimTypes.Role, "member", ClaimValueTypes.String)
};
var identity = new ClaimsIdentity(claims, "password");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
return new LoginResponse
{
Success = true
};
}
I would like to conditionally render a "Log Out" button on various parts of the site if the user has the cookie which authenticates the user. Additionally I'd like to fetch the Sid claim so I can deliver personalized messaging on some public parts of the site.
The problem I have is that the way I have been fetching the Sid only works if my controller or controller action has an [Authorize] attribute on it. Without the [Authorize] attribute, the claim is missing.
Code:
public static int? GetNullableAccountId(this ClaimsPrincipal principal)
{
var claim = principal.FindFirst((Claim c) => { return c.Type == ClaimTypes.Sid; });
if (claim == null)
return null;
return int.Parse(claim.Value);
}
// then in the controller I try to get the account id:
var accountId = accessor.HttpContext.User.GetNullableAccountId();
// always null even when I have a valid cookie
I swear that I didn't need the [Authorize] attribute for this to work in prior versions of ASP.NET Core, but I couldn't find anything meaningful in change logs.
Is there some trick to getting ASP.NET Core to build the user identity on all calls or am I taking the wrong approach all together?
It seems it was a silly mistake. I was invoking app.UseAuthentication() after app.UseMvc() when configuring my application builder.
The documentation actually explicitly states the following:
Call the UseAuthentication method before calling UseMvcWithDefaultRoute or UseMvc
Source: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-2.2#configuration

Single Sign On using ASP.NET Identity between 2 ASP.NET MVC projects

I have 2 web applications that both share the same main level domain as mentioned below so I can share cookies. Web.conifg in both the projects have same machine key and validation key. Since, I want to use Identity and NOT forms authenticaiton, I do not have an node in either of my web.config file. I am able to create the Auth cookie successully from SSO and can view the authorized pages in SSO but I am still redirected to SSO login when I try to access an authorized view in MVC project.
sso.domain.com - MVC Project
mvc.domain.com - MVC Project
I have a startup.cs file in my SSO and MVC project like below:
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(3),
LoginPath = new PathString("/Login"),
CookieName = "MyCookieName",
CookieDomain = ".domain.com"
});
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
}
}
Below is the code I have so far in the SSO project under the AccountController.cs. I call the IdentitySignin function below on validating the user against the database which creates the cookie:
private void IdentitySignin(string userId, string name, string providerKey = null, bool isPersistent = false)
{
var claims = new List<Claim>();
// create *required* claims
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
claims.Add(new Claim(ClaimTypes.Name, name));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
//get the expiry minutes from config or use the default value of 30 minutes
double expiryMinutes;
expiryMinutes = double.TryParse(ConfigurationManager.AppSettings["AuthCookieExpiryMinutes"], out expiryMinutes) ? expiryMinutes : 30;
// add to user here!
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = isPersistent,
ExpiresUtc = DateTime.UtcNow.AddMinutes(expiryMinutes),
IssuedUtc = DateTime.UtcNow
}, identity);
}
private void IdentitySignout()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie, DefaultAuthenticationTypes.ExternalCookie);
}
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private async Task<string> GetVerifiedUserIdAsync()
{
var result = await AuthenticationManager.AuthenticateAsync(
DefaultAuthenticationTypes.ApplicationCookie);
if (result != null && result.Identity != null
&& !String.IsNullOrEmpty(result.Identity.GetUserId()))
{
return result.Identity.GetUserId();
}
return null;
}
So, I figured out the reason why Single Sign On wasn't working between 2 MVC applications in-spite of both of them sharing the same machine and validation keys.
My SSO MVC application and the other MVC application were both using different version of OWIN and ASP.NET Identity DLLs. I used the Nuget to update the DLLS in one project but did not do the update in the other one.
I hope this helps someone who runs into this problem.
Just as an FYI, to share ASP.NET Identity Authentication between more than 1 application, make sure you have below things in EACH APP:
Same Machine Keys and Validation Keys in the web.config file
Same versions of OWIN and ASP.NET IDENTITY DLLs
SAME COOKIE NAME AND COOKIE DOMAIN in the Startup.cs
Both app needs to be on same domain to share the auth cookie
Most likely, you haven't set a shared machine key. The auth cookie is encrypted, and unless both sites share the same machine key, one cannot decrypt what the other encrypted. Add the following to both project's Web.config:
<sytem.web>
...
<machineKey validation="HMACSHA256" validationKey="[validationKey]" decryptionKey="[decryptionKey]" compatibilityMode="Framework45" />
To generate the keys, in IIS, click on the server in the left pane, and then the Machine Key control panel item. Choose your validation method (above, I've used HMACSHA256). I don't recommend using the default of SHA1 as that's ridiculously easy to crack. Then, on the right "Actions" panel, click "Generate Keys". Copy the two text box values to the appropriate attributes of this config element, and make sure they are the same for all projects that need to share the auth cookie.

Cookie without Identity Asp.net core

I'm currently working on a project that I don't use Identity.
The things is that this project should have a remember me option that allow user to automatically reconnect into the web site.
My problem is that I can't find any complete tutoriel to create a cookie without Identity.
If somebody have a good sample of code or tutoial :)
Thanks
In my project, I use AngularJS for Frontend and .Net Core API for Backend.
So, I don't need to configure pages for AccessDeniedPath, LoginPath and so on.
Here's what I do:
Configure the cookie in the startup class:
public void Configure(IApplicationBuilder app) {
//...
CookieAuthenticationOptions options = new CookieAuthenticationOptions();
options.AuthenticationScheme = "MyCookie";
options.AutomaticAuthenticate = true;
options.CookieName = "MyCookie";
app.UseCookieAuthentication(options);
//...
}
The login is like this:
[HttpPost, Route("Login")]
public IActionResult LogIn([FromBody]LoginModel login) {
//...
var identity = new ClaimsIdentity("MyCookie");
//add the login as the name of the user
identity.AddClaim(new Claim(ClaimTypes.Name, login.Login));
//add a list of roles
foreach (Role r in someList.Roles) {
identity.AddClaim(new Claim(ClaimTypes.Role, r.Name));
}
var principal = new ClaimsPrincipal(identity);
HttpContext.Authentication.SignInAsync("MyCookie", principal).Wait();
return Ok();
}
The logout is like this:
[HttpPost, Route("Logout")]
public async Task<IActionResult> LogOut() {
await HttpContext.Authentication.SignOutAsync("MyCookie");
return Ok();
}
Then you can use it like this:
[HttpPost]
[Authorize(Roles = "Role1,Role2,Role3")]
public IActionResult Post() {
//...
string userName = this.User.Identity.Name;
//...
}
*See that the method is authorized only for "Role1, Role2 and Role3". And see how to get the user name.
I think you are asking how to make a persistent cookie when the user logs in with a "Remember Me?" checkbox selected.
All the answers are on the right path - you'll ultimately invoke HttpContext.Authentication.SignInAsync, but the cookie middleware issues a session cookie by default. You'll need to pass along authentication properties as a third parameter to make the cookie persistent, for example:
HttpContext.Authentication.SignInAsync(
Options.Cookies.ApplicationCookieAuthenticationScheme,
userPrincipal,
new AuthenticationProperties { IsPersistent = isPersistent });
There is a pretty good article on this here: Using Cookie Middleware without ASP.NET Core Identity.
Basically what you do is set up the cookie handling middleware as if you were going to identify the user, but then you just create a ClaimsPrincipal object without asking the user to login. You pass that object to the SigninAsync method and it creates the cookie for you. If you follow this article you should be fine.

Categories

Resources