Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
431 lines (351 sloc)
15 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 Microsoft.AspNet.Identity; | |
using Microsoft.AspNet.Identity.EntityFramework; | |
using Microsoft.AspNet.Identity.Owin; | |
using Microsoft.Owin.Security; | |
using Microsoft.Owin.Security.Cookies; | |
using Microsoft.Owin.Security.OAuth; | |
using AsyncAwaitPain.WebApi.Models; | |
using AsyncAwaitPain.WebApi.Providers; | |
using AsyncAwaitPain.WebApi.Results; | |
using System.Linq; | |
namespace AsyncAwaitPain.WebApi.Controllers | |
{ | |
[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 => _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); | |
private set => _userManager = value; | |
} | |
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; } | |
// GET api/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?.LoginProvider | |
}; | |
} | |
// POST api/Account/Logout | |
[Route("Logout")] | |
public IHttpActionResult Logout() | |
{ | |
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType); | |
return Ok(); | |
} | |
// GET api/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 = user.Logins.Select(linkedAccount => new UserLoginInfoViewModel() | |
{ | |
LoginProvider = linkedAccount.LoginProvider, | |
ProviderKey = linkedAccount.ProviderKey | |
}).ToList(); | |
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 api/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 api/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); | |
return !result.Succeeded ? GetErrorResult(result) : Ok(); | |
} | |
// POST api/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?.Identity == null || | |
(ticket.Properties?.ExpiresUtc != null | |
&& 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)); | |
return result.Succeeded ? Ok() : GetErrorResult(result); | |
} | |
// POST api/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)); | |
} | |
return !result.Succeeded ? GetErrorResult(result) : Ok(); | |
} | |
// GET api/Account/ExternalLogin | |
[OverrideAuthentication] | |
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)] | |
[AllowAnonymous] | |
[Route("ExternalLogin", Name = "ExternalLogin")] | |
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 api/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>(); | |
const int strengthInBits = 256; | |
string state = generateState ? RandomOAuthStateGenerator.Generate(strengthInBits) : null; | |
foreach (AuthenticationDescription description in descriptions.Where(d => d.Caption.StartsWith("foo"))) | |
{ | |
ExternalLoginViewModel login = MakeExternalLoginViewModel(Request.RequestUri, returnUrl, state, description); | |
logins.Add(login); | |
} | |
return logins; | |
} | |
// POST api/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 api/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); | |
return result.Succeeded ? Ok() : GetErrorResult(result); | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
if (disposing && _userManager != null) | |
{ | |
_userManager.Dispose(); | |
_userManager = null; | |
} | |
base.Dispose(disposing); | |
} | |
#region Helpers | |
private IAuthenticationManager Authentication => Request.GetOwinContext().Authentication; | |
private IHttpActionResult GetErrorResult(IdentityResult result) | |
{ | |
if (result == null) return InternalServerError(); | |
if (result.Succeeded) return null; | |
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); | |
} | |
private class ExternalLoginData | |
{ | |
public string LoginProvider { get; private set; } | |
public string ProviderKey { get; private set; } | |
private 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) return claims; | |
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider)); | |
return claims; | |
} | |
public static ExternalLoginData FromIdentity(ClaimsIdentity identity) | |
{ | |
Claim providerKeyClaim = identity?.FindFirst(ClaimTypes.NameIdentifier); | |
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer) | |
|| String.IsNullOrEmpty(providerKeyClaim.Value) | |
||providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer) | |
{ | |
return null; | |
} | |
return new ExternalLoginData | |
{ | |
LoginProvider = providerKeyClaim.Issuer, | |
ProviderKey = providerKeyClaim.Value, | |
UserName = identity.FindFirstValue(ClaimTypes.Name) | |
}; | |
} | |
} | |
private static ExternalLoginViewModel MakeExternalLoginViewModel(Uri requestUrl, string returnUrl, string state, AuthenticationDescription description) | |
{ | |
return new ExternalLoginViewModel | |
{ | |
Name = description.Caption, | |
Url = Url.Route("ExternalLogin", new | |
{ | |
provider = description.AuthenticationType, | |
response_type = "token", | |
client_id = Startup.PublicClientId, | |
redirect_uri = new Uri(requestUrl, returnUrl).AbsoluteUri, | |
state | |
}), | |
State = state | |
}; | |
} | |
private static class RandomOAuthStateGenerator | |
{ | |
private static readonly 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.", nameof(strengthInBits)); | |
} | |
int strengthInBytes = strengthInBits / bitsPerByte; | |
byte[] data = new byte[strengthInBytes]; | |
_random.GetBytes(data); | |
return HttpServerUtility.UrlTokenEncode(data); | |
} | |
} | |
#endregion | |
} | |
} |