I am using Web API 2 with OWIN token based authentication. Only thing that is not working is authorization based on roles.
In my implementation of AuthorizationServerProvider.GrantResourceOwnerCredentials this is how i assign roles:
identity.AddClaim(client.ApplicationType == ApplicationTypes.WebClient
? new Claim(ClaimTypes.Role, "user")
: new Claim(ClaimTypes.Role, "admin"));
But in the Controller using [Authenticate(Roles="user")] just returns a Authorization denied message to the client. I checked the variables and this is whats inside
So the role seems to be there, but user.Claims is empty and IsInRole("user") also returns negative.
I found several questions here on stackoverflow and logic-wise i don't see what i missed. Only thing that comes to my mind is overwriting the Authorize Command but this is kind of needless as role based authorization seems to be integrated already...
EDIT: This is what my workig method looks like:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin") ?? "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
Client client;
using (var repo = new AuthRepository())
{
client = repo.FindClient(context.ClientId);
if (client.ApplicationType != ApplicationTypes.Service)
{
var user = await repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect." + context.UserName);
return;
}
}
}
Don't add the role claim directly, use the UserManager instead:
UserManagerInstance.AddToRole(userId, "admin");
That way the role will be persisted (to the AspNetUserRoles or whatever you have configured) so it will be there for the later requests. This doesn't happen if you add the claim directly, since you're adding it to an "instance" of your user identity that will die with the current request.
TO ANSWER YOUR FURTHER REQUIREMENTS:
If you want the claims codified on the ticket then you have to do this after adding the claims the way you're doing (in GrantResourceOwnerCredentials):
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "userId", "blah,blah" },
{ "role", "admin" }
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
This way you don't have to "persist" these kind of users
Of course you would have to override the TokenEndpoint method of the OAuthAuthorizationServerProvider in order to retrive those data on later request/responses.
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
Probably solved it somehow, but for me it works if I put it like this:
[Authorize(Roles = "user")]
[Route("")]
[HttpGet]
public async Task<IHttpActionResult> GetUserSpecificServers() { ... }
Related
I've been using the Antiforgery Cookie for .NET Core (https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery) and I'm always returning a new View after the login:
// - HomeController.cs
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Login()
{
ClaimsIdentity identity = new ClaimsIdentity("myAuthType");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("myScheme", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
return View();
}
The code is done so that each call/view generates a new X-XSRF-Cookie and sends to the browser, who sends the value back for some Api calls.
The code for that is:
// - Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(o => o.HeaderName = CrossSiteAntiForgery.Header);
services.AddMvc(o => {
o.Filters.AddService(typeof(CrossSiteAntiForgery));
});
}
// - CrossSiteAntiForgery.cs
public class CrossSiteAntiForgery : ResultFilterAttribute
{
public const string Cookie = "XSRF-TOKEN";
public const string Header = "X-XSRF-TOKEN";
private IAntiforgery _antiforgery;
public CrossSiteAntiForgery(IAntiforgery antiforgery)
{
_antiforgery = antiforgery;
}
public override void OnResultExecuting(ResultExecutingContext context)
{
AntiforgeryTokenSet tokens = _antiforgery.GetAndStoreTokens(context.HttpContext);
context.HttpContext.Response.Cookies.Append(Cookie, tokens.RequestToken, new CookieOptions() { HttpOnly = false });
}
}
But I wanted to login via Ajax just because. So I kept everything and changed just the Login:
[HttpPost("Api/User/Login"), ValidateAntiForgeryToken, Produces("application/json")]
public async Task<IActionResult> Login()
{
var userFromDB = DB.GetUser(1);
ClaimsIdentity identity = new ClaimsIdentity("myAuthType");
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("myScheme", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
return Json(userFromDB);
}
However now I get errors (Bad Request) because the XSRF tokens don't match. After some (a lot actually) digging, I think I found the issue, the browser does receive the Set-Cookie header properly from the ajax and sets the new cookie value, but this value is invalid because it seems whenever the _antiforgery.GetAndStoreTokens(HttpContext) is called, it uses the current User to generate the token somehow.
When I return a new View, the Context is updated with the new (logged in) User, the token is generated and sent to the browser and everything works perfectly.
When I use ajax and call HttpContext.SignInAsync(), the current User hasn't been updated yet and so the generated token will be invalid for the logged in User.
// - Login()
var user = HttpContext.User;
await HttpContext.SignInAsync("Login", principal,
new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(10),
AllowRefresh = true,
IsPersistent = true
});
var loggedInUser = HttpContext.User;
bool truth = user.Equals(loggedInUser); // - true
// - meaning anything that relies on the new logged in User is invalid from here on.
The error message in the .NET console is The provided antiforgery token was meant for a different claims-based user. I tried the suggestion here and it seems hacky, it works only for the second request onwards, meaning users would need to see a first "Bad Request" for every User change (Login and Logout).
How can I get/update the User after it has been logged in so the XSRF token generation works? Or, if there's a better solution, how can I solve this?
Edit: I'm working around it for now like this, in case anyone has the same issue:
public async Task<IActionResult> Login()
{
// - Login code omitted for brevity.
return RedirectToAction("LoginUnelegantWorkaround");
}
public IActionResult LoginUnelegantWorkaround()
{
var model = DB.FetchModelIWasGoingToReturnBefore();
return Json(model);
// - token is properly generated now since User has been updated, everything works
}
// - Same thing needed for Logout
I have two apps I'm working on. Both of them need to use both Windows authentication and anonymous access. so to do this, I edited the web.config to get rid of the authorization tag (with "deny users="?"") and only tagged a few actions with my custom authorization attribute. the trouble is, the server is "forgetting" me. so for instance, on the first app, one user reports that she has to attempt to access the control panel every other time she wants to edit. On the second one, I click login, I'm logged in, and then I click any other link (especially "save") and I'm logged out.
here's one of my custom authorization attributes:
public class AccountsAuthorizeITAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if(httpContext.User.Identity.IsAuthenticated == false)
{
return false;
}
if(httpContext.User.IsInRole("CT-IT"))
{
return true;
}
return false;
}
}
and to log in, I just have this in my _layout:
#Html.ActionLink("Login", "Login", "Login", new { returnURL = HttpContext.Current.Request.RawUrl }, null)
with this login controller:
public class LoginController : Controller
{
[AccountsAuthorizeIT]
public ActionResult Login(string returnURL)
{
return Redirect(returnURL);
}
}
What could cause this? Shouldn't my authentication be stored in the session variable, saved for (roughly) as long as the browser window is open? Do I need to tell the server to remember my data?
Shouldn't my authentication be stored in the session variable, saved
for (roughly) as long as the browser window is open? Do I need to tell
the server to remember my data?
I personally like to store them in Principle object as Claim using OWIN Cookie Middleware.
Here is the sample code. roleNames could be user's assigned Active Directory Group.
public void SignIn(User user, IList<string> roleNames)
{
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 (string roleName in roleNames)
{
claims.Add(new Claim(ClaimTypes.Role, roleName));
}
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthenticationType);
IOwinContext context = _context.Request.GetOwinContext();
IAuthenticationManager authenticationManager = context.Authentication;
authenticationManager.SignIn(identity);
}
Startup.cs
Then you register OWIN Cookie Middleware at start up.
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ApplicationCookie",
LoginPath = new PathString("/Account/Login")
});
}
}
If you store them in Principle object, you won't even need custom attribute AccountsAuthorizeITAttribute.
I've stumbled upon an issue where inconsistently the application redirects the user to Account/AccessDenied/ upon adding a social media authentication to the current logged in user. It seems to work the first time the user is logged in, then by trying to add another authentication method it returns the user to Account/AccessDenied?ReturnUrl=%2Fmanage%2Flinklogincallback.
My guess is that something is going wrong with the [Authorize] attribute, but only the second time I try adding external authentication method.
ManageController
[Authorize]
public class ManageController : Controller
{
//
// POST: /Manage/LinkLogin
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action("LinkLoginCallback", "Manage");
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return Challenge(properties, provider);
}
//
// GET: /Manage/LinkLoginCallback
[HttpGet]
public async Task<ActionResult> LinkLoginCallback()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return View("Error");
}
var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user));
if (info == null)
{
return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error });
}
var result = await _userManager.AddLoginAsync(user, info);
var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error;
return RedirectToAction(nameof(ManageLogins), new { Message = message });
}
}
Could it be the order of how startup.cs is arranged?
This is the request/response
#Rovdjuret's workaround helped me until it’s resolved by asp.net team. Here is my controller Login action:
public IActionResult Login(string returnUrl = null)
{
if (_signInManager.IsSignedIn(User))
{
// redirect to user profile page
return RedirectToAction(nameof(HomeFileController.Index), "HomeFile");
}
else
{
// clear Identity.External cookie
if (Request.Cookies["Identity.External"] != null)
{
Response.Cookies.Delete("Identity.External");
}
return View(new LoginViewModel{ ReturnUrl = returnUrl, RememberMe = true });
}
}
Update: In the latest version (as of May 2017) the cookies have prefix ".AspNetCore.". So cookie name should be ".AspNetCore.Identity.External"
I too faced the same issue. I was using the code from IdentityServer4 QuickStart sample from here
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
DisplayName = "Google",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = "xxx.apps.googleusercontent.com",
ClientSecret = "xxxx-Xxxxxxx"
});
I had to change the code to the following to fix the issue.
var CookieScheme= app.ApplicationServices.GetRequiredService<IOptions<IdentityOptions>>().Value.Cookies.ExternalCookieAuthenticationScheme;
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
DisplayName = "Google",
SignInScheme = CookieScheme,
ClientId = "xxx.apps.googleusercontent.com",
ClientSecret = "xxxx-Xxxxxxx"
});
Instead of just using the constant 'external' from the IdentityServerConstants.ExternalAUthenticationScheme I had to obtain the scheme used to identify external authentication cookies from the cookie options of the current identity system used by the app. That is what fixed the issue for me.
I've got confirmed by aspnet team working on Security repo that this is a bug (see this issue) and resolved until next release.
A temporary workaround is to set a cookie named
Identity.External
to null, which is created upon adding external login to your account.
if (Request.Cookies["Identity.External"] != null)
{
Response.Cookies.Delete("Identity.External");
}
workaround helped me until it’s resolved by asp.net team
// GET: /Account/AccessDenied
[HttpGet]
[AllowAnonymous]
public IActionResult AccessDenied(string returnUrl = null)
{
// workaround
if (Request.Cookies["Identity.External"] != null)
{
return RedirectToAction(nameof(ExternalLoginCallback), returnUrl);
}
return RedirectToAction(nameof(Login));
}
If you've set config.SignIn.RequireConfirmedEmail = true in Startup.cs and the EmailConfirmed field is false for an externally authenticated user (e.g. Facebook login), on subsequent logins, you'll be directed to the Account/AccessDenied/ action method.
My question is kind of complex so bear with me as I try to lay it out nicely what I am struggling with.
Goal
Have an ASP.NET website that lets users register & sign-in via Username/Password or Social (Facebook, Twitter, Google, etc) that also has an API. This API needs to be locked down with [Authorize]. The API needs to be able to be accessed by mobile clients (Android, iOS, etc) that can be signed in via Username/Password or Social (Facebook, Twitter, Google, etc).
Background
So I have done sites that can do one or two things from my goal but not all together. There are great examples online and built in examples into VS projects that show how to let the user register and sign-in via social apps but they are only for the website and not for mobile. I have done a website that an Android app uses Username/Password to authenticate with that API, but nothing with OAuth or Social credentials.
I started out using this page as a reference but I have no clue how to take that and make it work for my website logging in and for my mobile app logging in.
This guy makes it sound so easy but doesn't show any code for this.
Question
Is there a tutorial or GitHub example somewhere that can get me to my goal? I basically want a website where people can register a username/password or use their social account AND also let the user do the same (register & login) via a mobile device. The mobile device will basically just use the API to push/pull data, but I am unsure how to incorporate social logins with my API. I assume I need to use OAuth and go that route but I cannot find any good examples that show how to do this for both web and mobile.
Or maybe is the right solution is to have the webpage be all cookie auth and the API be a separate "web site" and be all token auth and they both tie to the same database?
I've successfully done this very task within my own ASP.NET MVC application using ASP.NET Identity, but then hit the issue you mention: I need this to work using Web API as well so that my mobile app can interact natively.
I was unfamiliar with the article you linked, but after reading through it, I noticed that a lot of the work and code their is not necessary and complicates functionality that already exists within ASP.NET Identity.
Here are my recommendations, and I am assuming you are using ASP.NET Identity V2 which is equivalent to the packages surrounding MVC5 (not the new MVC6 vNext). This will allow both your website AND mobile application via API to authenticate both with a local login (username/password) and an external OAuth provider both from MVC web views on your website and through Web API calls from your mobile application:
Step 1. When creating your project, insure you have both the required packages for MVC and Web API included. In the ASP.NET Project Selection dialog you will have the option to select the checkboxes, insure MVC and Web API are both checked. If you didn't already do this when you created your project, I would recommend creating a new project and migrating your existing code over versus searching and manually adding the dependencies and template code.
Step 2. Inside your Startup.Auth.cs file, you will need code to tell OWIN to use cookie authentication, allow external sign in cookies, and support OAuth bearer tokens (This is how Web API calls will authenticate). These are relevant excerpts from my working project codebase:
Startup.Auth.cs
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/account/externallogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
//AllowInsecureHttp = false
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
app.UseTwitterAuthentication(
consumerKey: "Twitter API Key",
consumerSecret: "Twitter API Secret");
app.UseFacebookAuthentication(
appId: "Facebook AppId",
appSecret: "Facebook AppSecret");
In the above code I currently support Twitter and Facebook as external authentication providers; however, you can add additional external providers with the app.UserXYZProvider calls and additional libraries and they will plug and play with the code I provide here.
Step 3. Inside your WebApiConfig.cs file, you must configure the HttpConfiguration to supress default host authentication and support OAuth bearer tokens. To explain, this tells your application to differentiate authentication types between MVC and Web API, this way you can use the typical cookie flow for the website, meanwhile your application will accept bearer tokens in the form of OAuth from the Web API without complaining or other issues.
WebApiConfig.cs
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Step 4. You need an AccountController (or equivalently purposed controller) for both MVC and Web API. In my project I have two AccountController files, one MVC controller inheriting from the base Controller class, and another AccountController inheriting from ApiController that is in a Controllers.API namespace to keep things clean. I am using the standard template AccountController code from the Web API and MVC projects. Here is the API version of the Account Controller:
AccountController.cs (Controllers.API namespace)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Disco.Models.API;
using Disco.Providers;
using Disco.Results;
using Schloss.AspNet.Identity.Neo4j;
using Disco.Results.API;
namespace Disco.Controllers.API
{
[Authorize]
[RoutePrefix("api/account")]
public class AccountController : ApiController
{
private const string LocalLoginProvider = "Local";
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
// GET account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("userinfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
// POST account/Logout
[Route("logout")]
public IHttpActionResult Logout()
{
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return Ok();
}
// GET account/ManageInfo?returnUrl=%2F&generateState=true
[Route("manageinfo")]
public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
{
IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (UserLoginInfo linkedAccount in await UserManager.GetLoginsAsync(User.Identity.GetUserId()))
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
// POST account/ChangePassword
[Route("changepassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/SetPassword
[Route("setpassword")]
public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/AddExternalLogin
[Route("addexternallogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
if (ticket == null || ticket.Identity == null || (ticket.Properties != null
&& ticket.Properties.ExpiresUtc.HasValue
&& ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
{
return BadRequest("External login failure.");
}
ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
if (externalData == null)
{
return BadRequest("The external login is already associated with an account.");
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RemoveLogin
[Route("removelogin")]
public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result;
if (model.LoginProvider == LocalLoginProvider)
{
result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
}
else
{
result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(model.LoginProvider, model.ProviderKey));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// GET account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("externallogin", Name = "ExternalLoginAPI")]
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
{
if (error != null)
{
return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
}
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(provider, this);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
// GET account/ExternalLogins?returnUrl=%2F&generateState=true
[AllowAnonymous]
[Route("externallogins")]
public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationDescription description in descriptions)
{
ExternalLoginViewModel login = new ExternalLoginViewModel
{
Name = description.Caption,
Url = Url.Route("ExternalLogin", new
{
provider = description.AuthenticationType,
response_type = "token",
client_id = Startup.PublicClientId,
redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
state = state
}),
State = state
};
logins.Add(login);
}
return logins;
}
// POST account/Register
[AllowAnonymous]
[Route("register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("registerexternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing && _userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
base.Dispose(disposing);
}
#region Helpers
private IAuthenticationManager Authentication
{
get { return Request.GetOwinContext().Authentication; }
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
private class ExternalLoginData
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string UserName { get; set; }
public IList<Claim> GetClaims()
{
IList<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
if (UserName != null)
{
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
}
return claims;
}
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
|| String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
}
private static class RandomOAuthStateGenerator
{
private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
public static string Generate(int strengthInBits)
{
const int bitsPerByte = 8;
if (strengthInBits % bitsPerByte != 0)
{
throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits");
}
int strengthInBytes = strengthInBits / bitsPerByte;
byte[] data = new byte[strengthInBytes];
_random.GetBytes(data);
return HttpServerUtility.UrlTokenEncode(data);
}
}
#endregion
}
}
Step 5. You also need to create an ApplicationOAuthProvider so the server can generate and validate OAuth tokens. This is provided in the WebAPI sample project. This is my version of the file:
ApplicationOAuthProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Butler.Models;
using Schloss.AspNet.Identity.Neo4j;
namespace Butler.Providers
{
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
//Uri expectedRootUri = new Uri(context.Request.Uri, "/");
//if (expectedRootUri.AbsoluteUri == context.RedirectUri)
//{
context.Validated();
//}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
}
Also included is the ChallengeResult, which the Web API arm of your application will use to handle challenges provided by the external login providers to authenticate your user:
ChallengeResult.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
namespace Butler.Results
{
public class ChallengeResult : IHttpActionResult
{
public ChallengeResult(string loginProvider, ApiController controller)
{
LoginProvider = loginProvider;
Request = controller.Request;
}
public string LoginProvider { get; set; }
public HttpRequestMessage Request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
Request.GetOwinContext().Authentication.Challenge(LoginProvider);
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
With that set of code, you will be able to HTTP GET and HTTP POST the routes on the API version of the AccountController to register a user, login using username and password to receive a Bearer token, add/remove external logins, manage external logins, and most importantly for your issue, authenticate by passing in an external login token in exchange for an OAuth bearer token for you application.
You may want to have a look at this series of articles to see whether it covers your goal:
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
by Taiseer Joudeh (who also frequently answers questions on SO)
The articles are about creating a token based authentication service using OWIN and one of the parts cover using external logins (such as Facebook and Google+). The examples are primarily centered around a web application as consumer of the web service, but it should work on mobile applications as well. The articles have a GitHub project associated and a very active comment section, where hardly any question goes unanswered.
Hope this may lead you to your goal.
I am adding this as a seperate answer to the second portion of your question to say that YES you can have two separate projects tied to the same database and simply have the MVC/Web Forms website project use all cookie authentication and then have a separate Web API project that is all token authentication.
In my longer answer with source code examples what I have basically done is combine the two separate projects into one project so as to avoid redundant model code and controller code. In my case this made more sense for me; however, I am inclined to say that it is up to personal preference and the needs of your project to dictate whether to maintain two separate projects, one website and one web API endpoint, or to combine them.
ASP.NET was designed to be very flexible and plug and play as a middleware and I can attest that my project has existed and functioned exactly as intended with the code in two separate projects and now as one combined project.
I'm using ASP.NET Identity 2.0 with a custom store of my own.
I'm noticing that store operations are inefficiently called multiple times, notably upon log in.
Here is my sign in code (pretty much what's included with the default template):
[AllowAnonymous]
public async Task<ActionResult> LogIn(LogInModel model)
{
if(model!=null && (!string.IsNullOrEmpty(model.Email) || !string.IsNullOrEmpty(model.Password)))
{
model.DisplayValidationMessages=true;
if(ModelState.IsValid)
{
BaseApplicationUser user=await UserManager.FindAsync(model.Email,model.Password);
if(user!=null)
{
await SignInAsync(user,model.RememberMe);
return Redirect((model.ContinueUrl??"/")+"#"+model.State.UrlEncode());
}
model.ErrorMessage="Those credentials are invalid, please try again";
}
}
return View(model);
}
protected async Task SignInAsync(BaseApplicationUser user,bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent=isPersistent },
await user.GenerateUserIdentityAsync(UserManager)
);
}
My user is extended as follow:
public class BaseApplicationUser:User
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<BaseApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
ClaimsIdentity userIdentity=await manager.CreateIdentityAsync(this,DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
ConfigureAuth:
public void ConfigureAuth(IAppBuilder app)
{
[...]
// Configure the db context and user manager to use a single instance per request
//app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<BaseApplicationUserManager>((_options,_context) => BaseApplicationUserManager.Create(usersStore,_options,_context));
app.CreatePerOwinContext<BaseApplicationRoleManager>((_options,_context) => BaseApplicationRoleManager.Create(rolesStore,_options,_context));
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType=DefaultAuthenticationTypes.ApplicationCookie,
LoginPath=new PathString("/Authentication/LogIn"),
CookieSecure=CookieSecureOption.Always,
CookieHttpOnly=true,
Provider=new CookieAuthenticationProvider {
OnValidateIdentity=SecurityStampValidator.OnValidateIdentity<BaseApplicationUserManager,BaseApplicationUser>(
TimeSpan.FromMinutes(30),
(_manager,_user) => _user.GenerateUserIdentityAsync(_manager)
)
}
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
[...]
}
Problems:
Upon sign in, the user is retrieved using BaseApplicationUser user=await UserManager.FindAsync(model.Email,model.Password); which is absolutely normal.
When ClaimsIdentity userIdentity=await manager.CreateIdentityAsync(this,DefaultAuthenticationTypes.ApplicationCookie); is called, it is passed a BaseApplicationUser so it shouldn't need to call FindByIdAsync 3 times (!!!) on the user store as it does. This is dumbly suboptimal. In fact it should even call this, since the user object is already retrieved.
My solution is really "implementation specific" (since I implemented my own identity stores for MongoDB, I set the cache at this level, which allows me better control than a generic solution), but if this can be of any help to anybody, I posted my source code to http://pastebin.com/MV0F4MUA
You then "call" the cache for each request by setting this is your ConfigureAuth method:
app.CreatePerOwinContext<BaseApplicationUserManager>((_options,_context) => BaseApplicationUserManager.Create(new AuthenticationProviderRequestCache<BaseApplicationUser>(authenticationProvider),_options,_context));
WARNING: You can't simply copy/paste my code into your solution, you need to understand it to adapt it to your needs.
I am having same issue, after some searching(not too long because no one ever posted a proper explanation for what is going on) I ended up writing my own version of CreateIdentityAsync method using the original method implemented in Microsoft.AspNet.Identity.Core namespace as a ref., here it is:
public ClaimsIdentity CreateAsync(IdentityUser user, string authenticationType)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
ClaimsIdentity claimsIdentity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.NameIdentifier, ClaimTypes.Role);
claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), "http://www.w3.org/2001/XMLSchema#string"));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, user.UserName, "http://www.w3.org/2001/XMLSchema#string"));
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity", "http://www.w3.org/2001/XMLSchema#string"));
return claimsIdentity;
}
But this is still a workaround till I can understand where the call to UserStore.FindByIdAsync() is coming from
I just had to solve this for an older web app, so here's my solution for anyone still searching. Look at the archived AspNet Identity source (https://github.com/aspnet/AspNetIdentity). The reason for the three calls to IUserStore.FindByIdAsync() is due to Microsoft.AspNet.Identity.ClaimsIdentityFactory passing the user id to three methods when adding claims, which in turn call IUserStore.FindByIdAsync(). The thing is, in my case the calling method has the user object already with everything I need, so my solution was to override these methods to accept the user object and proceed as normal.
public class UserManager : Microsoft.AspNet.Identity.UserManager<Employee, int>
{
public UserManager(IUserStore<Employee, int> store) : base(store)
{
ClaimsIdentityFactory = new ClaimsIdentityFactory();
}
...
public override async Task<ClaimsIdentity> CreateIdentityAsync(Employee user, string authenticationType)
{
if (user != null && /* user is active, etc */)
{
var userIdentity = await ClaimsIdentityFactory.CreateAsync(this, user, authenticationType);
...
return userIdentity;
}
else
{
return null;
}
}
...
public async Task<string> GetSecurityStampAsync(Employee user)
{
var securityStore = Store as IUserSecurityStampStore<Employee, int>;
if (securityStore == null)
{
throw new NotSupportedException("User Store Not IUserSecurityStampStore");
}
return await securityStore.GetSecurityStampAsync(user).WithCurrentCulture();
}
public async Task<IList<string>> GetRolesAsync(Employee user)
{
var userRoleStore = Store as IUserRoleStore<Employee, int>;
if (userRoleStore == null)
{
throw new NotSupportedException("User Store Not IUserRoleStore");
}
return await userRoleStore.GetRolesAsync(user).WithCurrentCulture();
}
public virtual async Task<IList<Claim>> GetClaimsAsync(Employee user)
{
var claimStore = Store as IUserClaimStore<Employee, int>;
if (claimStore == null)
{
throw new NotSupportedException("User Store Not IUserClaimStore");
}
return await claimStore.GetClaimsAsync(user).WithCurrentCulture();
}
}
public class ClaimsIdentityFactory : Microsoft.AspNet.Identity.ClaimsIdentityFactory<Employee, int>
{
...
public override async Task<ClaimsIdentity> CreateAsync(Microsoft.AspNet.Identity.UserManager<Employee, int> manager, Employee user, string authenticationType)
{
if (manager == null)
{
throw new ArgumentNullException("manager");
}
if (user == null)
{
throw new ArgumentNullException("user");
}
var id = new ClaimsIdentity(authenticationType, UserNameClaimType, RoleClaimType);
id.AddClaim(new Claim(UserIdClaimType, ConvertIdToString(user.Id), ClaimValueTypes.String));
id.AddClaim(new Claim(UserNameClaimType, user.UserName, ClaimValueTypes.String));
id.AddClaim(new Claim(IdentityProviderClaimType, DefaultIdentityProviderClaimValue, ClaimValueTypes.String));
if (manager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(SecurityStampClaimType, await (manager as UserManager).GetSecurityStampAsync(user).WithCurrentCulture()));
}
if (manager.SupportsUserRole)
{
IList<string> roles = await (manager as UserManager).GetRolesAsync(user).WithCurrentCulture();
foreach (string roleName in roles)
{
id.AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String));
}
}
if (manager.SupportsUserClaim)
{
id.AddClaims(await (manager as UserManager).GetClaimsAsync(user).WithCurrentCulture());
}
return id;
}
}