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
@model IEnumerable<AuthenticationDescription>
@using Yavsc.ViewModels.Account
@model LoginViewModel
<div class="jumbotron">
<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">
<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>
@Html.AntiForgeryToken()
</form>
}
</div>

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

@ -6,10 +6,10 @@
<language-layout>
<ul class="nav navbar-nav navbar-right">
<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>
<button type="submit" class="btn btn-link navbar-btn navbar-link">@SR["Logout"]</button>
<button type="submit" class="navbar-link">@SR["Logout"]</button>
</li>
</ul>
</language-layout>
@ -18,8 +18,9 @@
else
{ <language-layout>
<ul class="nav navbar-nav navbar-right">
<li><a 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="Register">@SR["Register"]</a></li>
<li><a class="navbar-link" asp-controller="Account" asp-action="SignIn">@SR["Login"]</a></li>
</ul>
</language-layout>
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
@ -50,14 +51,30 @@ namespace Yavsc.Controllers
_twilioSettings = twilioSettings.Value;
_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);
*/
}
//
// GET: /Account/Login
[HttpGet]
public IActionResult Login(string returnUrl = null)
[HttpGet("~/signout"), HttpPost("~/signout")]
public async Task<IActionResult> SignOut(string returnUrl = "/")
{
ViewData["ReturnUrl"] = returnUrl;
return View();
// Instruct the cookies middleware to delete the local cookie created when the user agent
// 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()
@ -65,15 +82,45 @@ namespace Yavsc.Controllers
return View();
}
// POST: /Account/Login
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn(string provider, string returnUrl)
{
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
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
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.");
return RedirectToLocal(returnUrl);
return RedirectToLocal(model.ReturnUrl);
}
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)
{
@ -98,11 +145,9 @@ namespace Yavsc.Controllers
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/Register
[HttpGet]
@ -171,7 +216,7 @@ namespace Yavsc.Controllers
var info = await _signInManager.GetExternalLoginInfoAsync();
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.
@ -210,8 +255,11 @@ namespace Yavsc.Controllers
var token_type = info.ExternalPrincipal.FindFirstValue("token_type");
var expires_in = info.ExternalPrincipal.FindFirstValue("expires_in");
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email,
Name = name });
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel
{
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.Net.Http.Headers;
using Yavsc.Auth;
using Yavsc.Extensions;
using Yavsc.Formatters;
using Yavsc.Models;
using Yavsc.Providers;
using Yavsc.Services;
@ -66,8 +68,6 @@ namespace Yavsc
"~/bower_components/dropzone/dist/min/basic.min.css",
"~/bower_components/dropzone/dist/min/dropzone.min.css"
));
}
}
@ -155,7 +155,8 @@ namespace Yavsc
RSAKeyUtils.GetKeyParameters(keyParamsFileInfo.Name) :
RSAKeyUtils.GenerateKeyAndSave(keyParamsFileInfo.Name);
key = new RsaSecurityKey(keyParams);
services.Configure<SharedAuthenticationOptions>(options => {
services.Configure<SharedAuthenticationOptions>(options =>
{
options.SignInScheme = "ServerCookie";
});
services.Configure<TokenAuthOptions>(
@ -187,8 +188,10 @@ namespace Yavsc
configure.PersistKeysToFileSystem(
new DirectoryInfo(Configuration["DataProtection:Keys:Dir"]));
});
services.AddAuthentication();
services.AddAuthentication(options => {
options.SignInScheme = "ServerCookie"; }
);
// Add framework services.
services.AddEntityFramework()
.AddNpgsql()
@ -232,7 +235,8 @@ namespace Yavsc
{
options.AddPolicy("AdministratorOnly", policy => policy.RequireRole(Constants.AdminGroupName));
options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName));
options.AddPolicy("API", policy => {
options.AddPolicy("API", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
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
{
@ -376,19 +374,6 @@ namespace Yavsc
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.UseStaticFiles().UseWebSockets();
@ -397,21 +382,38 @@ namespace Yavsc
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.
app.UseCookieAuthentication(options => {
branch.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.AuthenticationScheme = "ServerCookie";
options.CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie";
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
app.UseFacebookAuthentication(options =>
branch.UseFacebookAuthentication(options =>
{
options.AppId = Configuration["Authentication:Facebook:AppId"];
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";
});
});
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

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

Loading…