réactive oidc, définitivement.

main
Paul Schneider 10 years ago
parent d5bcfd19f6
commit ebed4d2c50
7 changed files with 356 additions and 98 deletions

@ -1,17 +1,53 @@
 
@using Microsoft.AspNet.Http.Authentication @using Microsoft.AspNet.Http.Authentication
@model IEnumerable<AuthenticationDescription> @using Yavsc.ViewModels.Account
@model LoginViewModel
<div class="jumbotron"> <div class="jumbotron">
<h1>Authentication</h1> <h1>Authentication</h1>
<p class="lead text-left">Sign in using one of these external providers:</p> <hr/>
<h2 class="lead text-left">Use a local account to log in</h2>
<form action="/login" method="post" class="form-horizontal" role="form">
@foreach (var description in Model) { <div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
<div class="form-group">
<label for="UserName" class="col-md-2 control-label">User name</label>
<div class="col-md-10">
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label for="Password" class="col-md-2 control-label">Password</label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<button type="submit" class="btn btn-lg btn-success">Login</button>
</div>
</div>
<p>
<a asp-action="Register">Register as a new user?</a>
</p>
<p>
<a asp-action="ForgotPassword">Forgot your password?</a>
</p>
<input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
@Html.AntiForgeryToken()
</form>
<hr/>
<h2 class="lead text-left">Sign in using one of these external providers:</h2>
@foreach (var description in Model.ExternalProviders) {
<form action="/signin" method="post"> <form action="/signin" method="post">
<input type="hidden" name="Provider" value="@description.AuthenticationScheme" /> <input type="hidden" name="Provider" value="@description.AuthenticationScheme" />
<input type="hidden" name="ReturnUrl" value="@ViewBag.ReturnUrl" /> <input type="hidden" name="ReturnUrl" value="@Model.ReturnUrl" />
<button class="btn btn-lg btn-success" type="submit">Connect using @description.DisplayName</button> <button class="btn btn-lg btn-success" type="submit">Connect using @description.DisplayName</button>
@Html.AntiForgeryToken()
</form> </form>
} }
</div> </div>

@ -44,9 +44,9 @@
</div> </div>
<div class="navbar-collapse collapse"> <div class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a asp-controller="Home" asp-action="Index">@SR["Home"]</a></li> <li><a asp-controller="Home" asp-action="Index" class="navbar-link">@SR["Home"]</a></li>
<li><a asp-controller="Home" asp-action="About">@SR["About"] @SiteSettings.Value.Title</a> </li> <li><a asp-controller="Home" asp-action="About" class="navbar-link">@SR["About"] @SiteSettings.Value.Title</a> </li>
<li><a asp-controller="Home" asp-action="Contact">@SR["Contact"]</a></li> <li><a asp-controller="Home" asp-action="Contact" class="navbar-link">@SR["Contact"]</a></li>
</ul> </ul>
@await Html.PartialAsync("_LoginPartial") @await Html.PartialAsync("_LoginPartial")
</div> </div>

@ -6,10 +6,10 @@
<language-layout> <language-layout>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li> <li>
<a asp-controller="Manage" asp-action="Index" title="Manage">@SR["Hello"] @User.GetUserName()!</a> <a asp-controller="Manage" class="navbar-link" asp-action="Index" title="Manage">@SR["Hello"] @User.GetUserName()!</a>
</li> </li>
<li> <li>
<button type="submit" class="btn btn-link navbar-btn navbar-link">@SR["Logout"]</button> <button type="submit" class="navbar-link">@SR["Logout"]</button>
</li> </li>
</ul> </ul>
</language-layout> </language-layout>
@ -18,8 +18,9 @@
else else
{ <language-layout> { <language-layout>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li><a asp-controller="Account" asp-action="Register">@SR["Register"]</a></li> <li><a class="navbar-link" asp-controller="Account" asp-action="Register">@SR["Register"]</a></li>
<li><a asp-controller="Account" asp-action="Login">@SR["Login"]</a></li> <li><a class="navbar-link" asp-controller="Account" asp-action="SignIn">@SR["Login"]</a></li>
</ul> </ul>
</language-layout> </language-layout>
} }

@ -5,6 +5,7 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
@ -50,14 +51,30 @@ namespace Yavsc.Controllers
_twilioSettings = twilioSettings.Value; _twilioSettings = twilioSettings.Value;
_logger = loggerFactory.CreateLogger<AccountController>(); _logger = loggerFactory.CreateLogger<AccountController>();
} }
[HttpGet("~/signin")]
public ActionResult SignIn(string returnUrl = "/")
{
return View("SignIn", new LoginViewModel
{
ReturnUrl = returnUrl,
ExternalProviders = _signInManager.GetExternalAuthenticationSchemes()
});
/* When using an external login provider :
// Request a redirect to the external login provider.
var redirectUrl = returnUrl ?? "/";
var properties = _signInManager.ConfigureExternalAuthenticationProperties(OpenIdConnectDefaults.AuthenticationScheme, redirectUrl);
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, properties);
*/
}
// [HttpGet("~/signout"), HttpPost("~/signout")]
// GET: /Account/Login public async Task<IActionResult> SignOut(string returnUrl = "/")
[HttpGet]
public IActionResult Login(string returnUrl = null)
{ {
ViewData["ReturnUrl"] = returnUrl; // Instruct the cookies middleware to delete the local cookie created when the user agent
return View(); // is redirected from the identity provider after a successful authorization flow and
// to redirect the user agent to the identity provider to sign out.
await _signInManager.SignOutAsync();
return Redirect(returnUrl);
} }
public IActionResult Forbidden() public IActionResult Forbidden()
@ -65,15 +82,45 @@ namespace Yavsc.Controllers
return View(); return View();
} }
// POST: /Account/Login [HttpPost("~/signin")]
[HttpPost] public async Task<IActionResult> SignIn(string provider, string returnUrl)
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{ {
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid) // Note: the "provider" parameter corresponds to the external
// authentication provider choosen by the user agent.
if (string.IsNullOrEmpty(provider))
{ {
_logger.LogWarning("null provider");
ModelState.AddModelError("provider", "provider cannot be null");
return new BadRequestObjectResult(ModelState);
}
// Note: the "returnUrl" parameter corresponds to the endpoint the user agent
// will be redirected to after a successful authentication and not
// the redirect_uri of the requesting client application.
if (string.IsNullOrEmpty(returnUrl))
{
_logger.LogWarning($"null returnUrl ({provider}) ");
ModelState.AddModelError("returnUrl", "returnUrl cannot be null");
return new BadRequestObjectResult(ModelState);
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
// Request a redirect to the external login provider.
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
[HttpPost("~/login")]
public async Task<IActionResult> LocalLogin(LoginViewModel model)
{
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout // This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true // To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false); var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
@ -81,11 +128,11 @@ namespace Yavsc.Controllers
{ {
_logger.LogInformation(1, "User logged in."); _logger.LogInformation(1, "User logged in.");
return RedirectToLocal(returnUrl); return RedirectToLocal(model.ReturnUrl);
} }
if (result.RequiresTwoFactor) if (result.RequiresTwoFactor)
{ {
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); return RedirectToAction(nameof(SendCode), new { ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
} }
if (result.IsLockedOut) if (result.IsLockedOut)
{ {
@ -98,11 +145,9 @@ namespace Yavsc.Controllers
return View(model); return View(model);
} }
} }
// If we got this far, something failed, redisplay form // If we got this far, something failed, redisplay form
return View(model); return View(model);
} }
// //
// GET: /Account/Register // GET: /Account/Register
[HttpGet] [HttpGet]
@ -171,7 +216,7 @@ namespace Yavsc.Controllers
var info = await _signInManager.GetExternalLoginInfoAsync(); var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) if (info == null)
{ {
return RedirectToAction(nameof(Login)); return RedirectToAction(nameof(SignIn));
} }
// Sign in the user with this external login provider if the user already has a login. // Sign in the user with this external login provider if the user already has a login.
@ -210,8 +255,11 @@ namespace Yavsc.Controllers
var token_type = info.ExternalPrincipal.FindFirstValue("token_type"); var token_type = info.ExternalPrincipal.FindFirstValue("token_type");
var expires_in = info.ExternalPrincipal.FindFirstValue("expires_in"); var expires_in = info.ExternalPrincipal.FindFirstValue("expires_in");
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email, return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel
Name = name }); {
Email = email,
Name = name
});
} }
} }

@ -0,0 +1,128 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.Data.Entity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Yavsc.Models;
namespace Yavsc.Providers {
public sealed class AuthorizationProvider : OpenIdConnectServerProvider {
private ILogger _logger;
public AuthorizationProvider(ILoggerFactory loggerFactory) {
_logger = loggerFactory.CreateLogger<AuthorizationProvider>();
}
public override Task MatchEndpoint(MatchEndpointContext context) {
// Note: by default, OpenIdConnectServerHandler only handles authorization requests made to the authorization endpoint.
// This context handler uses a more relaxed policy that allows extracting authorization requests received at
// /connect/authorize/accept and /connect/authorize/deny (see AuthorizationController.cs for more information).
if (context.Options.AuthorizationEndpointPath.HasValue &&
context.Request.Path.StartsWithSegments(context.Options.AuthorizationEndpointPath)) {
context.MatchesAuthorizationEndpoint();
}
return Task.FromResult<object>(null);
}
public override async Task ValidateAuthorizationRequest(ValidateAuthorizationRequestContext context) {
// Note: the OpenID Connect server middleware supports the authorization code, implicit and hybrid flows
// but this authorization provider only accepts response_type=code authorization/authentication requests.
// You may consider relaxing it to support the implicit or hybrid flows. In this case, consider adding
// checks rejecting implicit/hybrid authorization requests when the client is a confidential application.
if (!context.Request.IsAuthorizationCodeFlow()) {
context.Rejected(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "Only the authorization code flow is supported by this authorization server");
return;
}
var database = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
_logger.LogInformation($"Searching fo app id {context.ClientId}");
// Retrieve the application details corresponding to the requested client_id.
var application = await (from entity in database.Applications
where entity.ApplicationID == context.ClientId
select entity).SingleOrDefaultAsync(context.HttpContext.RequestAborted);
if (application == null) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Application not found in the database: ensure that your client_id is correct");
return;
}
if (!string.IsNullOrEmpty(context.Request.RedirectUri) &&
!string.Equals(context.Request.RedirectUri, application.RedirectUri, StringComparison.Ordinal)) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Invalid redirect_uri");
return;
}
context.Validated();
}
public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
// Note: the OpenID Connect server middleware supports authorization code, refresh token, client credentials
// and resource owner password credentials grant types but this authorization provider uses a safer policy
// rejecting the last two ones. You may consider relaxing it to support the ROPC or client credentials grant types.
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) {
context.Rejected(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only authorization code and refresh token grant types " +
"are accepted by this authorization server");
return;
}
// Note: client authentication is not mandatory for non-confidential client applications like mobile apps
// (except when using the client credentials grant type) but this authorization server uses a safer policy
// that makes client authentication mandatory and returns an error if client_id or client_secret is missing.
// You may consider relaxing it to support the resource owner password credentials grant type
// with JavaScript or desktop applications, where client credentials cannot be safely stored.
// In this case, call context.Skip() to inform the server middleware the client is not trusted.
if (string.IsNullOrEmpty(context.Request.ClientId) || string.IsNullOrEmpty(context.Request.ClientSecret)) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Missing credentials: ensure that your credentials were correctly " +
"flowed in the request body or in the authorization header");
return;
}
var database = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
// Retrieve the application details corresponding to the requested client_id.
var application = await (from entity in database.Applications
where entity.ApplicationID == context.ClientId
select entity).SingleOrDefaultAsync(context.HttpContext.RequestAborted);
if (application == null) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Application not found in the database: ensure that your client_id is correct");
return;
}
if (!string.Equals(context.Request.ClientSecret, application.Secret, StringComparison.Ordinal)) {
context.Rejected(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Invalid credentials: ensure that you specified a correct client_secret");
return;
}
context.Validated();
}
}
}

@ -35,8 +35,10 @@ using Microsoft.Extensions.PlatformAbstractions;
using Microsoft.Extensions.WebEncoders; using Microsoft.Extensions.WebEncoders;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Yavsc.Auth; using Yavsc.Auth;
using Yavsc.Extensions;
using Yavsc.Formatters; using Yavsc.Formatters;
using Yavsc.Models; using Yavsc.Models;
using Yavsc.Providers;
using Yavsc.Services; using Yavsc.Services;
@ -66,8 +68,6 @@ namespace Yavsc
"~/bower_components/dropzone/dist/min/basic.min.css", "~/bower_components/dropzone/dist/min/basic.min.css",
"~/bower_components/dropzone/dist/min/dropzone.min.css" "~/bower_components/dropzone/dist/min/dropzone.min.css"
)); ));
} }
} }
@ -155,7 +155,8 @@ namespace Yavsc
RSAKeyUtils.GetKeyParameters(keyParamsFileInfo.Name) : RSAKeyUtils.GetKeyParameters(keyParamsFileInfo.Name) :
RSAKeyUtils.GenerateKeyAndSave(keyParamsFileInfo.Name); RSAKeyUtils.GenerateKeyAndSave(keyParamsFileInfo.Name);
key = new RsaSecurityKey(keyParams); key = new RsaSecurityKey(keyParams);
services.Configure<SharedAuthenticationOptions>(options => { services.Configure<SharedAuthenticationOptions>(options =>
{
options.SignInScheme = "ServerCookie"; options.SignInScheme = "ServerCookie";
}); });
services.Configure<TokenAuthOptions>( services.Configure<TokenAuthOptions>(
@ -187,8 +188,10 @@ namespace Yavsc
configure.PersistKeysToFileSystem( configure.PersistKeysToFileSystem(
new DirectoryInfo(Configuration["DataProtection:Keys:Dir"])); new DirectoryInfo(Configuration["DataProtection:Keys:Dir"]));
}); });
services.AddAuthentication();
services.AddAuthentication(options => {
options.SignInScheme = "ServerCookie"; }
);
// Add framework services. // Add framework services.
services.AddEntityFramework() services.AddEntityFramework()
.AddNpgsql() .AddNpgsql()
@ -232,7 +235,8 @@ namespace Yavsc
{ {
options.AddPolicy("AdministratorOnly", policy => policy.RequireRole(Constants.AdminGroupName)); options.AddPolicy("AdministratorOnly", policy => policy.RequireRole(Constants.AdminGroupName));
options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName));
options.AddPolicy("API", policy => { options.AddPolicy("API", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(OpenIdConnectConstants.Claims.Scope, "api-resource-controller"); policy.RequireClaim(OpenIdConnectConstants.Claims.Scope, "api-resource-controller");
}); });
@ -340,12 +344,6 @@ namespace Yavsc
} }
} }
// Create a new branch where the registered middleware will be executed only for API calls.
/* MapWhenExtensions.MapWhen(app,(Func<Microsoft.AspNet.Http.HttpContext, bool>)(context =>
context.Request.Path.StartsWithSegments((PathString)new PathString((string)"/api"))),
(Action<IApplicationBuilder>)( branch => { }));
*/
var googleOptions = new GoogleOptions var googleOptions = new GoogleOptions
{ {
@ -376,19 +374,6 @@ namespace Yavsc
googleOptions.Scope.Add("https://www.googleapis.com/auth/calendar"); googleOptions.Scope.Add("https://www.googleapis.com/auth/calendar");
var udirinfo = new DirectoryInfo(Configuration["Site:UserFiles:RootDir"]);
if (!udirinfo.Exists)
throw new Exception($"Configuration value for Site:UserFiles:RootDir : {udirinfo.FullName}");
app.UseFileServer(new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(
udirinfo.FullName),
RequestPath = new PathString(Constants.UserFilesRequestPath),
EnableDirectoryBrowsing = true
});
app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear()); app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear());
app.UseStaticFiles().UseWebSockets(); app.UseStaticFiles().UseWebSockets();
@ -397,21 +382,38 @@ namespace Yavsc
app.UseIdentity(); app.UseIdentity();
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.RequireHttpsMetadata = false;
options.Audience = siteSettings.Value.Audience;
options.Authority = siteSettings.Value.Authority;
});
});
// Create a new branch where the registered middleware will be executed only for API calls.
app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
// Create a new branch where the registered middleware will be executed only for non API calls. // Create a new branch where the registered middleware will be executed only for non API calls.
app.UseCookieAuthentication(options => { branch.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true; options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true; options.AutomaticChallenge = true;
options.AuthenticationScheme = "ServerCookie"; options.AuthenticationScheme = "ServerCookie";
options.CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie"; options.CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie";
options.ExpireTimeSpan = TimeSpan.FromMinutes(5); options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.LoginPath = new PathString("/Account/Login"); options.LoginPath = new PathString("/signin");
options.LogoutPath = new PathString("/signout");
}); });
app.UseMiddleware<GoogleMiddleware>(googleOptions); branch.UseMiddleware<GoogleMiddleware>(googleOptions);
// Facebook // Facebook
app.UseFacebookAuthentication(options => branch.UseFacebookAuthentication(options =>
{ {
options.AppId = Configuration["Authentication:Facebook:AppId"]; options.AppId = Configuration["Authentication:Facebook:AppId"];
options.AppSecret = Configuration["Authentication:Facebook:AppSecret"]; options.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
@ -419,6 +421,44 @@ namespace Yavsc
options.UserInformationEndpoint = "https://graph.facebook.com/v2.5/me?fields=id,name,email,first_name,last_name"; options.UserInformationEndpoint = "https://graph.facebook.com/v2.5/me?fields=id,name,email,first_name,last_name";
}); });
});
app.UseOpenIdConnectServer(options =>
{
options.Provider = new AuthorizationProvider(loggerFactory);
// Register the certificate used to sign the JWT tokens.
/* options.SigningCredentials.AddCertificate(
assembly: typeof(Startup).GetTypeInfo().Assembly,
resource: "Mvc.Server.Certificate.pfx",
password: "Owin.Security.OpenIdConnect.Server"); */
// options.SigningCredentials.AddKey(key);
// Note: see AuthorizationController.cs for more
// information concerning ApplicationCanDisplayErrors.
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.AutomaticChallenge = true;
options.AuthorizationEndpointPath = new PathString("/connect/authorize");
options.TokenEndpointPath = new PathString("/connect/authorize/accept");
options.UseSlidingExpiration = true;
options.AllowInsecureHttp = true;
options.AuthenticationScheme = "oidc"; // was = OpenIdConnectDefaults.AuthenticationScheme;
options.LogoutEndpointPath = new PathString("/connect/logout");
/* options.ValidationEndpointPath = new PathString("/connect/introspect"); */
});
var udirinfo = new DirectoryInfo(Configuration["Site:UserFiles:RootDir"]);
if (!udirinfo.Exists)
throw new Exception($"Configuration value for Site:UserFiles:RootDir : {udirinfo.FullName}");
app.UseFileServer(new FileServerOptions()
{
FileProvider = new PhysicalFileProvider(
udirinfo.FullName),
RequestPath = new PathString(Constants.UserFilesRequestPath),
EnableDirectoryBrowsing = true
});
/* Generic OAuth (here GitHub): options.Notifications = new OAuthAuthenticationNotifications /* Generic OAuth (here GitHub): options.Notifications = new OAuthAuthenticationNotifications

@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Http.Authentication;
namespace Yavsc.ViewModels.Account namespace Yavsc.ViewModels.Account
{ {
@ -14,5 +16,8 @@ namespace Yavsc.ViewModels.Account
[Display(Name = "Remember me?")] [Display(Name = "Remember me?")]
public bool RememberMe { get; set; } public bool RememberMe { get; set; }
public string ReturnUrl { get; set; }
public IEnumerable<AuthenticationDescription> ExternalProviders { get; set; }
} }
} }

Loading…