From 6e301e52d8806170fe5a28388b91b9a78881c043 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 31 May 2016 14:44:07 +0200 Subject: [PATCH] =?UTF-8?q?tout=20du=20bon:=20-=20mobile=20app=20declarati?= =?UTF-8?q?on=20(WIP)=20-=20login/logout=20depuis=20la=20m=C3=AAme=20resou?= =?UTF-8?q?rce,=20sans=20bug=20=20=20(note:=20appliquer=20AllowAnonymous?= =?UTF-8?q?=20=C3=A0=20la=20classe=20=3D>=20methodes=20d'attribut=20Author?= =?UTF-8?q?ize=20non=20redirig=C3=A9es)=20-=20les=20m=C3=A9dias=20des=20po?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yavsc/Views/Blogs/Index.cshtml | 16 -- Yavsc/Views/Blogspot/Create.cshtml | 9 +- Yavsc/Views/Blogspot/Details.cshtml | 28 +-- Yavsc/Views/Blogspot/Edit.cshtml | 5 +- Yavsc/Views/Blogspot/Index.cshtml | 2 +- Yavsc/Views/Shared/SignIn.cshtml | 1 - Yavsc/src/ApiController/AccountController.cs | 1 + Yavsc/src/ApiController/GCMController.cs | 37 ++++ Yavsc/src/AuthorisationHandlers.cs | 9 +- Yavsc/src/Constants.cs | 2 +- Yavsc/src/Controllers/AccountController.cs | 4 +- Yavsc/src/Controllers/BlogspotController.cs | 62 +++---- .../src/Controllers/FrontOfficeController.cs | 2 +- Yavsc/src/Controllers/OAuthController.cs | 162 ++++++++++++------ Yavsc/src/Helpers/TagHelpers.cs | 2 +- Yavsc/src/Model/ApplicationDbContext.cs | 6 + .../Model/Identity/MobileAppDeclaration.cs | 21 +++ Yavsc/src/Services/MessageServices.cs | 3 +- Yavsc/src/Startup.cs | 128 ++++++++------ Yavsc/wwwroot/css/site.css | 9 +- 20 files changed, 321 insertions(+), 188 deletions(-) delete mode 100755 Yavsc/Views/Blogs/Index.cshtml create mode 100644 Yavsc/src/ApiController/GCMController.cs diff --git a/Yavsc/Views/Blogs/Index.cshtml b/Yavsc/Views/Blogs/Index.cshtml deleted file mode 100755 index 7f3bb0a8..00000000 --- a/Yavsc/Views/Blogs/Index.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@model IEnumerable - -@{ - ViewData["Title"] = "Blog spot"; -} - -
- @foreach (var entry in Model) { -
-

@entry.Title

-
    -
  • Sample pages using ASP.NET MVC 6
  • -
-
- } -
diff --git a/Yavsc/Views/Blogspot/Create.cshtml b/Yavsc/Views/Blogspot/Create.cshtml index 9a53baf6..c168ae13 100644 --- a/Yavsc/Views/Blogspot/Create.cshtml +++ b/Yavsc/Views/Blogspot/Create.cshtml @@ -10,20 +10,20 @@

Blog


-
+
- +
- +
@@ -31,7 +31,7 @@
- +
@@ -40,7 +40,6 @@
- @Html.Hidden("AuthorId")
diff --git a/Yavsc/Views/Blogspot/Details.cshtml b/Yavsc/Views/Blogspot/Details.cshtml index 6a43700b..57e1f2ab 100644 --- a/Yavsc/Views/Blogspot/Details.cshtml +++ b/Yavsc/Views/Blogspot/Details.cshtml @@ -8,6 +8,18 @@

Blog


+
+ @Html.DisplayNameFor(model => model.title) +
+
+ @Html.DisplayFor(model => model.title) +
+
+ @Html.DisplayNameFor(model => model.photo) +
+
+ @Html.DisplayFor(model => model.photo) +
@SR["Author"]
@@ -18,7 +30,7 @@ Contenu
-
@@ -27,12 +39,7 @@
@Html.DisplayFor(model => model.modified)
-
- @Html.DisplayNameFor(model => model.photo) -
-
- @Html.DisplayFor(model => model.photo) -
+
@Html.DisplayNameFor(model => model.posted)
@@ -45,12 +52,7 @@
@Html.DisplayFor(model => model.rate)
-
- @Html.DisplayNameFor(model => model.title) -
-
- @Html.DisplayFor(model => model.title) -
+
@Html.DisplayNameFor(model => model.visible)
diff --git a/Yavsc/Views/Blogspot/Edit.cshtml b/Yavsc/Views/Blogspot/Edit.cshtml index 468d96af..a2fc8661 100644 --- a/Yavsc/Views/Blogspot/Edit.cshtml +++ b/Yavsc/Views/Blogspot/Edit.cshtml @@ -182,8 +182,9 @@ editorcontenu.on('text-change',function(delta,source){
- - +
+ +
diff --git a/Yavsc/Views/Blogspot/Index.cshtml b/Yavsc/Views/Blogspot/Index.cshtml index ca57b4b9..40d0f735 100644 --- a/Yavsc/Views/Blogspot/Index.cshtml +++ b/Yavsc/Views/Blogspot/Index.cshtml @@ -41,7 +41,7 @@ @item.Author?.UserName - @Html.DisplayFor(modelItem => item.title) + @item.title @Html.DisplayFor(modelItem => item.modified) diff --git a/Yavsc/Views/Shared/SignIn.cshtml b/Yavsc/Views/Shared/SignIn.cshtml index 44f54ce2..ab088444 100644 --- a/Yavsc/Views/Shared/SignIn.cshtml +++ b/Yavsc/Views/Shared/SignIn.cshtml @@ -68,7 +68,6 @@ @foreach (var description in Model.ExternalProviders) { - @Html.AntiForgeryToken() diff --git a/Yavsc/src/ApiController/AccountController.cs b/Yavsc/src/ApiController/AccountController.cs index 0f9ada61..1c197a94 100644 --- a/Yavsc/src/ApiController/AccountController.cs +++ b/Yavsc/src/ApiController/AccountController.cs @@ -14,6 +14,7 @@ namespace Yavsc.WebApi.Controllers [Authorize,Route("~/api/account")] public class ApiAccountController : Controller { + private UserManager _userManager; private readonly SignInManager _signInManager; diff --git a/Yavsc/src/ApiController/GCMController.cs b/Yavsc/src/ApiController/GCMController.cs new file mode 100644 index 00000000..a2bbec9e --- /dev/null +++ b/Yavsc/src/ApiController/GCMController.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Mvc; +using Microsoft.Extensions.Logging; +using Yavsc.Models; + +public class GCMController : Controller { + ILogger _logger; + ApplicationDbContext _context; + + public GCMController (ApplicationDbContext context, + ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _context = context; + } + + [Authorize] + public void Register (GoogleCloudMobileDeclaration declaration) + { + if (_context.GCMDevices.Any(d => d.RegistrationId == declaration.RegistrationId)) + { + var alreadyRegisteredDevice = _context.GCMDevices.FirstOrDefault(d => d.RegistrationId == declaration.RegistrationId); + // Assert alreadyRegisteredDevice != null + if (alreadyRegisteredDevice != declaration) { + _context.GCMDevices.Update(declaration); + _context.SaveChanges(); + } // else nothing to do. + } + else + { + _context.GCMDevices.Add(declaration); + _context.SaveChanges(); + } + } + +} \ No newline at end of file diff --git a/Yavsc/src/AuthorisationHandlers.cs b/Yavsc/src/AuthorisationHandlers.cs index c99a9278..215504ec 100644 --- a/Yavsc/src/AuthorisationHandlers.cs +++ b/Yavsc/src/AuthorisationHandlers.cs @@ -70,10 +70,11 @@ public class BlogViewHandler : AuthorizationHandler context.Succeed(requirement); else if (context.User.Identity.IsAuthenticated) if (resource.AuthorId == context.User.GetUserId()) - context.Succeed(requirement); - // TODO else if (resource.Circles && context.User belongs to + context.Succeed(requirement); + else if (resource.visible) + // TODO && ( resource.Circles == null || context.User belongs to resource.Circles ) + context.Succeed(requirement); } - } public class CommandViewHandler : AuthorizationHandler @@ -137,4 +138,6 @@ public class BlogViewHandler : AuthorizationHandler } } + + } \ No newline at end of file diff --git a/Yavsc/src/Constants.cs b/Yavsc/src/Constants.cs index 97ed8062..99f759b9 100644 --- a/Yavsc/src/Constants.cs +++ b/Yavsc/src/Constants.cs @@ -24,8 +24,8 @@ namespace Yavsc public const string AdminGroupName = "Administrator"; public const string BlogModeratorGroupName = "Moderator"; public const string FrontOfficeGroupName = "FrontOffice"; - public const string UserBlogFilesDir= "Blog"; public const string UserBillsFilesDir= "Bills"; + public const string UserFilesDir = "UserFiles"; public const string GCMNotificationUrl = "https://gcm-http.googleapis.com/gcm/send"; private static readonly string[] GoogleScopes = { "openid", "profile", "email" }; diff --git a/Yavsc/src/Controllers/AccountController.cs b/Yavsc/src/Controllers/AccountController.cs index a0b55fd2..2a054380 100644 --- a/Yavsc/src/Controllers/AccountController.cs +++ b/Yavsc/src/Controllers/AccountController.cs @@ -18,8 +18,7 @@ using Yavsc.ViewModels.Account; namespace Yavsc.Controllers { - [AllowAnonymous] - [ServiceFilter(typeof(LanguageActionFilter))] + [ServiceFilter(typeof(LanguageActionFilter)),AllowAnonymous] public class AccountController : Controller { private readonly UserManager _userManager; @@ -138,6 +137,7 @@ namespace Yavsc.Controllers [ValidateAntiForgeryToken] public async Task LogOff(string returnUrl = null) { + await HttpContext.Authentication.SignOutAsync("ServerCookie"); await _signInManager.SignOutAsync(); _logger.LogInformation(4, "User logged out."); if (returnUrl==null) return RedirectToAction(nameof(HomeController.Index), "Home"); diff --git a/Yavsc/src/Controllers/BlogspotController.cs b/Yavsc/src/Controllers/BlogspotController.cs index e48dd8d2..eaf42ca1 100644 --- a/Yavsc/src/Controllers/BlogspotController.cs +++ b/Yavsc/src/Controllers/BlogspotController.cs @@ -14,8 +14,7 @@ using Microsoft.Extensions.OptionsModel; namespace Yavsc.Controllers { - [ServiceFilter(typeof(LanguageActionFilter)), - AllowAnonymous] + [ServiceFilter(typeof(LanguageActionFilter))] public class BlogspotController : Controller { ILogger _logger; @@ -40,31 +39,30 @@ namespace Yavsc.Controllers public IActionResult Index(string id) { if (!string.IsNullOrEmpty(id)) - return UserPosts(id); + return UserPosts(id); return View(_context.Blogspot.Include( - b=>b.Author - ).Where(p=>p.visible)); + b => b.Author + ).Where(p => p.visible)); } [Route("/Title/{id?}")] public IActionResult Title(string id) { - return View("Index", _context.Blogspot.Include( - b=>b.Author - ).Where(x=>x.title==id).ToList()); + return View("Index", _context.Blogspot.Include( + b => b.Author + ).Where(x => x.title == id).ToList()); } [Route("/Blog/{id?}")] public IActionResult UserPosts(string id) { - if (User.IsSignedIn()) - return View("Index", _context.Blogspot.Include( - b=>b.Author - ).Where(x=>x.Author.UserName==id).ToList()); + return View("Index", _context.Blogspot.Include( + b => b.Author + ).Where(x => x.Author.UserName == id).ToList()); return View("Index", _context.Blogspot.Include( - b=>b.Author - ).Where(x=>x.Author.UserName==id && x.visible).ToList()); + b => b.Author + ).Where(x => x.Author.UserName == id && x.visible).ToList()); } // GET: Blog/Details/5 public IActionResult Details(long? id) @@ -75,7 +73,7 @@ namespace Yavsc.Controllers } Blog blog = _context.Blogspot.Include( - b=>b.Author + b => b.Author ).Single(m => m.Id == id); if (blog == null) { @@ -86,19 +84,21 @@ namespace Yavsc.Controllers } // GET: Blog/Create + [Authorize("Authenticated")] public IActionResult Create() { - return View( new Blog { AuthorId = User.GetUserId() } ); + return View(); } // POST: Blog/Create - [HttpPost] - [ValidateAntiForgeryToken,Authorize] + [HttpPost, Authorize("Authenticated"), ValidateAntiForgeryToken] public IActionResult Create(Blog blog) { blog.modified = blog.posted = DateTime.Now; blog.rate = 0; - + blog.AuthorId = User.GetUserId(); + _logger.LogWarning($"Post from: {blog.AuthorId}"); + ModelState.ClearValidationState("AuthorId"); if (ModelState.IsValid) { blog.posted = DateTime.Now; @@ -106,10 +106,10 @@ namespace Yavsc.Controllers _context.SaveChanges(); return RedirectToAction("Index"); } - _logger.LogWarning("Invalid Blog entry ..."); + _logger.LogWarning("Invalid Blog posted ..."); return View(blog); } - + [Authorize("Authenticated")] // GET: Blog/Edit/5 public async Task Edit(long? id) { @@ -118,7 +118,7 @@ namespace Yavsc.Controllers return HttpNotFound(); } - Blog blog = _context.Blogspot.Include(x=>x.Author).Single(m => m.Id == id); + Blog blog = _context.Blogspot.Include(x => x.Author).Single(m => m.Id == id); if (blog == null) { return HttpNotFound(); @@ -135,7 +135,7 @@ namespace Yavsc.Controllers // POST: Blog/Edit/5 [HttpPost] - [ValidateAntiForgeryToken] + [ValidateAntiForgeryToken,Authorize("Authenticated")] public IActionResult Edit(Blog blog) { if (ModelState.IsValid) @@ -146,18 +146,19 @@ namespace Yavsc.Controllers blog.modified = DateTime.Now; _context.Update(blog); _context.SaveChanges(); - ViewData["StatusMessage"]="Post modified"; + ViewData["StatusMessage"] = "Post modified"; return RedirectToAction("Index"); } // TODO Else hit me hard - else { - ViewData["StatusMessage"]="Access denied ..."; + else + { + ViewData["StatusMessage"] = "Access denied ..."; } } return View(blog); } // GET: Blog/Delete/5 - [ActionName("Delete")] + [ActionName("Delete"),Authorize("Authenticated")] public IActionResult Delete(long? id) { if (id == null) @@ -166,7 +167,7 @@ namespace Yavsc.Controllers } Blog blog = _context.Blogspot.Include( - b=>b.Author + b => b.Author ).Single(m => m.Id == id); if (blog == null) { @@ -177,13 +178,14 @@ namespace Yavsc.Controllers } // POST: Blog/Delete/5 - [HttpPost, ActionName("Delete"), Authorize] + [HttpPost, ActionName("Delete"), Authorize("Authenticated")] [ValidateAntiForgeryToken] public IActionResult DeleteConfirmed(long id) { Blog blog = _context.Blogspot.Single(m => m.Id == id); var auth = _authorizationService.AuthorizeAsync(User, blog, new EditRequirement()); - if (auth.Result) { + if (auth.Result) + { _context.Blogspot.Remove(blog); _context.SaveChanges(); } diff --git a/Yavsc/src/Controllers/FrontOfficeController.cs b/Yavsc/src/Controllers/FrontOfficeController.cs index c956d7a4..01a957dc 100644 --- a/Yavsc/src/Controllers/FrontOfficeController.cs +++ b/Yavsc/src/Controllers/FrontOfficeController.cs @@ -9,7 +9,7 @@ using Yavsc.Models.Booking; namespace Yavsc.Controllers { - [ServiceFilter(typeof(LanguageActionFilter)), AllowAnonymous, + [ServiceFilter(typeof(LanguageActionFilter)), Route("do")] public class FrontOfficeController : Controller { diff --git a/Yavsc/src/Controllers/OAuthController.cs b/Yavsc/src/Controllers/OAuthController.cs index 71c63377..6ff146c1 100644 --- a/Yavsc/src/Controllers/OAuthController.cs +++ b/Yavsc/src/Controllers/OAuthController.cs @@ -27,7 +27,7 @@ namespace Yavsc.Controllers ApplicationDbContext _context; UserManager _userManager; -ILogger _logger; + ILogger _logger; private readonly SignInManager _signInManager; private TokenAuthOptions _tokenOptions; @@ -46,13 +46,15 @@ ILogger _logger; [HttpGet("~/signin")] - public ActionResult SignIn(string returnUrl = null) { + public ActionResult SignIn(string returnUrl = null, string target = null) + { // 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. return View("SignIn", new LoginViewModel { ReturnUrl = returnUrl, + AfterLoginRedirectUrl = target, ExternalProviders = HttpContext.GetExternalProviders() }); /* Note: When using an external login provider, redirect the query : @@ -61,49 +63,72 @@ ILogger _logger; */ } + [HttpGet("~/authenticate")] + public ActionResult Authenticate(string returnUrl = null) + { + return SignIn("/Account/ExternalLoginCallback",returnUrl); + } + + [HttpGet("~/forbidden")] + public ActionResult Forbidden(string returnUrl = null) + { + return SignIn("/Account/ExternalLoginCallback",returnUrl); + } + [HttpPost("~/signin")] - public IActionResult SignIn( string Provider, string ReturnUrl, string AfterLoginRedirectUrl) { + public IActionResult SignIn(string Provider, string ReturnUrl, string AfterLoginRedirectUrl) + { // Note: the "provider" parameter corresponds to the external // authentication provider choosen by the user agent. - if (string.IsNullOrEmpty(Provider)) { + if (string.IsNullOrEmpty(Provider)) + { _logger.LogWarning("Provider not specified"); return HttpBadRequest(); } - if (!_signInManager.GetExternalAuthenticationSchemes().Any(x=>x.AuthenticationScheme==Provider)) { + if (!_signInManager.GetExternalAuthenticationSchemes().Any(x => x.AuthenticationScheme == Provider)) + { _logger.LogWarning($"Provider not found : {Provider}"); return HttpBadRequest(); } - // 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("ReturnUrl not specified"); - return HttpBadRequest(); - } // 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 - - // If AfterLoginRedirectUrl is non null, - // This is a web interface access, - // and the wanted redirection - // after the successfull authentication - if (AfterLoginRedirectUrl!=null) { - ReturnUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = AfterLoginRedirectUrl }); + + // 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)) + { + + // If AfterLoginRedirectUrl is non null, + // This is a web interface access, + /// Assert (model.ReturnUrl==null) + /// and the wanted redirection + // after the successfull authentication + if (AfterLoginRedirectUrl != null) + { + ReturnUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = AfterLoginRedirectUrl }); + } + else + { + _logger.LogWarning("ReturnUrl not specified"); + return HttpBadRequest(); + } } - + var properties = _signInManager.ConfigureExternalAuthenticationProperties(Provider, ReturnUrl); - + return new ChallengeResult(Provider, properties); } [HttpGet("~/signout"), HttpPost("~/signout")] - public async Task SignOut() { + public async Task SignOut() + { // Instruct the cookies middleware to delete the local cookie created // when the user agent is redirected from the external identity provider // after a successful authentication flow (e.g Google or Facebook). @@ -132,23 +157,27 @@ ILogger _logger; } - [HttpGet("~/connect/authorize"), HttpPost("~/connect/authorize")] - public async Task Authorize(CancellationToken cancellationToken) { + [HttpGet("~/connect/authorize"), HttpPost("~/connect/authorize")] + public async Task Authorize(CancellationToken cancellationToken) + { // Note: when a fatal error occurs during the request processing, an OpenID Connect response // is prematurely forged and added to the ASP.NET context by OpenIdConnectServerHandler. // You can safely remove this part and let ASOS automatically handle the unrecoverable errors // by switching ApplicationCanDisplayErrors to false in Startup.cs. var response = HttpContext.GetOpenIdConnectResponse(); - if (response == null) { - _logger.LogError("GetOpenIdConnectResponse is null"); + if (response == null) + { + _logger.LogError("GetOpenIdConnectResponse is null"); return View("OidcError", response); } // Extract the authorization request from the ASP.NET environment. var request = HttpContext.GetOpenIdConnectRequest(); - if (request == null) { - _logger.LogError("An internal error has occurred, GetOpenIdConnectRequest is null"); - return View("OidcError", new OpenIdConnectMessage { + if (request == null) + { + _logger.LogError("An internal error has occurred, GetOpenIdConnectRequest is null"); + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal error has occurred" }); @@ -160,10 +189,13 @@ ILogger _logger; // To work around this limitation, the OpenID Connect request is automatically saved in the cache and will be // restored by the OpenID Connect server middleware after the external authentication process has been completed. - if (!User.Identities.Any(identity => identity.IsAuthenticated)) { - _logger.LogWarning("new ChallengeResult"); - return new ChallengeResult(new AuthenticationProperties { - RedirectUri = Url.Action(nameof(Authorize), new { + if (!User.Identities.Any(identity => identity.IsAuthenticated)) + { + _logger.LogWarning("new ChallengeResult"); + return new ChallengeResult(new AuthenticationProperties + { + RedirectUri = Url.Action(nameof(Authorize), new + { request_id = request.GetUniqueIdentifier() }) }); @@ -174,9 +206,11 @@ ILogger _logger; // In theory, this null check shouldn't be needed, but a race condition could occur if you // manually removed the application details from the database after the initial check made by ASOS. var application = await GetApplicationAsync(request.ClientId, cancellationToken); - if (application == null) { + if (application == null) + { _logger.LogError("Details concerning the calling client application cannot be found in the database"); - return View("OidcError", new OpenIdConnectMessage { + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.InvalidClient, ErrorDescription = "Details concerning the calling client application cannot be found in the database" }); @@ -187,15 +221,19 @@ ILogger _logger; } [Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken] - public async Task Accept(CancellationToken cancellationToken) { + public async Task Accept(CancellationToken cancellationToken) + { var response = HttpContext.GetOpenIdConnectResponse(); - if (response != null) { + if (response != null) + { return View("OidcError", response); } var request = HttpContext.GetOpenIdConnectRequest(); - if (request == null) { - return View("OidcError", new OpenIdConnectMessage { + if (request == null) + { + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal error has occurred" }); @@ -207,7 +245,8 @@ ILogger _logger; // Copy the claims retrieved from the external identity provider // (e.g Google, Facebook, a WS-Fed provider or another OIDC server). - foreach (var claim in HttpContext.User.Claims) { + foreach (var claim in HttpContext.User.Claims) + { // Allow ClaimTypes.Name to be added in the id_token. // ClaimTypes.NameIdentifier is automatically added, even if its // destination is not defined or doesn't include "id_token". @@ -218,7 +257,8 @@ ILogger _logger; OpenIdConnectConstants.Destinations.IdentityToken); } */ - if (claim.Type == ClaimTypes.Name) { + if (claim.Type == ClaimTypes.Name) + { claim.WithDestination("code"); claim.WithDestination("id_token"); } @@ -226,8 +266,10 @@ ILogger _logger; } var application = await GetApplicationAsync(request.ClientId, cancellationToken); - if (application == null) { - return View("OidcError", new OpenIdConnectMessage { + if (application == null) + { + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.InvalidClient, ErrorDescription = "Details concerning the calling client application cannot be found in the database" }); @@ -238,7 +280,7 @@ ILogger _logger; // the whole delegation chain from the resource server (see ResourceController.cs). identity.Actor = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); identity.Actor.AddClaim(ClaimTypes.NameIdentifier, application.ApplicationID); - identity.Actor.AddClaim(ClaimTypes.Name, application.DisplayName,"code id_token"); + identity.Actor.AddClaim(ClaimTypes.Name, application.DisplayName, "code id_token"); var properties = new AuthenticationProperties(); @@ -271,15 +313,19 @@ ILogger _logger; } [Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken] - public IActionResult Deny(CancellationToken cancellationToken) { + public IActionResult Deny(CancellationToken cancellationToken) + { var response = HttpContext.GetOpenIdConnectResponse(); - if (response != null) { + if (response != null) + { return View("OidcError", response); } var request = HttpContext.GetOpenIdConnectRequest(); - if (request == null) { - return View("OidcError", new OpenIdConnectMessage { + if (request == null) + { + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal error has occurred" }); @@ -289,7 +335,8 @@ ILogger _logger; // Notify AspNet.Security.OpenIdConnect.Server that the authorization grant has been denied. // Note: OpenIdConnectServerHandler will automatically take care of redirecting // the user agent to the client application using the appropriate response_mode. - HttpContext.SetOpenIdConnectResponse(new OpenIdConnectMessage { + HttpContext.SetOpenIdConnectResponse(new OpenIdConnectMessage + { Error = "access_denied", ErrorDescription = "The authorization grant has been denied by the resource owner", RedirectUri = request.RedirectUri, @@ -301,9 +348,11 @@ ILogger _logger; } [HttpGet("~/connect/logout")] - public async Task Logout() { + public async Task Logout() + { var response = HttpContext.GetOpenIdConnectResponse(); - if (response != null) { + if (response != null) + { _logger.LogError("GetOpenIdConnectResponse is null"); return View("OidcError", response); } @@ -314,9 +363,11 @@ ILogger _logger; var identity = await HttpContext.Authentication.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationScheme); var request = HttpContext.GetOpenIdConnectRequest(); - if (request == null) { + if (request == null) + { _logger.LogError("An internal error has occurred"); - return View("OidcError", new OpenIdConnectMessage { + return View("OidcError", new OpenIdConnectMessage + { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal error has occurred" }); @@ -327,7 +378,8 @@ ILogger _logger; [HttpPost("~/connect/logout")] [ValidateAntiForgeryToken] - public async Task Logout(CancellationToken cancellationToken) { + public async Task Logout(CancellationToken cancellationToken) + { // Instruct the cookies middleware to delete the local cookie created // when the user agent is redirected from the external identity provider // after a successful authentication flow (e.g Google or Facebook). @@ -348,7 +400,7 @@ ILogger _logger; where application.ApplicationID == identifier select application).SingleOrDefaultAsync(cancellationToken); } -private async Task GetCurrentUserAsync() + private async Task GetCurrentUserAsync() { return await _userManager.FindByIdAsync(HttpContext.User.GetUserId()); } diff --git a/Yavsc/src/Helpers/TagHelpers.cs b/Yavsc/src/Helpers/TagHelpers.cs index a470a72a..6d9c577b 100644 --- a/Yavsc/src/Helpers/TagHelpers.cs +++ b/Yavsc/src/Helpers/TagHelpers.cs @@ -108,7 +108,7 @@ namespace Yavsc.Helpers var content = await GetContent(output); var markdown = content; var basePath = Base?.StartsWith("~") ?? false ? - "/"+Startup.UserFilesDirName + + Constants.UserFilesDir + Base.Substring(1) : Base; var html = Markdown(markdown, basePath); output.Content.SetHtmlContent(html ?? ""); diff --git a/Yavsc/src/Model/ApplicationDbContext.cs b/Yavsc/src/Model/ApplicationDbContext.cs index 363d8232..9b876f68 100644 --- a/Yavsc/src/Model/ApplicationDbContext.cs +++ b/Yavsc/src/Model/ApplicationDbContext.cs @@ -71,6 +71,12 @@ namespace Yavsc.Models /// /// tokens public DbSet Tokens { get; set; } + + /// + /// References all declared external GCM devices + /// + /// + public DbSet GCMDevices { get; set; } public Task ClearTokensAsync() { diff --git a/Yavsc/src/Model/Identity/MobileAppDeclaration.cs b/Yavsc/src/Model/Identity/MobileAppDeclaration.cs index ec886f5f..313575cb 100644 --- a/Yavsc/src/Model/Identity/MobileAppDeclaration.cs +++ b/Yavsc/src/Model/Identity/MobileAppDeclaration.cs @@ -9,7 +9,28 @@ public class GoogleCloudMobileDeclaration { public string DeviceOwnerId { get; set; } + public string Name { get; set; } + [ForeignKeyAttribute("DeviceOwnerId")] public virtual ApplicationUser DeviceOwner { get; set; } + + // override object.Equals + public override bool Equals (object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + var other = obj as GoogleCloudMobileDeclaration; + return RegistrationId == other.RegistrationId + && Name == other.Name; + } + + // override object.GetHashCode + public override int GetHashCode() + { + return (RegistrationId+Name).GetHashCode(); + } } diff --git a/Yavsc/src/Services/MessageServices.cs b/Yavsc/src/Services/MessageServices.cs index 7fc6ba05..e091d98a 100755 --- a/Yavsc/src/Services/MessageServices.cs +++ b/Yavsc/src/Services/MessageServices.cs @@ -29,7 +29,8 @@ namespace Yavsc.Services /// a MessageWithPayloadResponse, /// bool somethingsent = (response.failure == 0 && response.success > 0) /// - public async Task NotifyAsync(GoogleAuthSettings googleSettings, string registrationId, YaEvent ev) + public async Task + NotifyAsync(GoogleAuthSettings googleSettings, string registrationId, YaEvent ev) { MessageWithPayloadResponse response; try diff --git a/Yavsc/src/Startup.cs b/Yavsc/src/Startup.cs index 74f63c6b..fada84d6 100755 --- a/Yavsc/src/Startup.cs +++ b/Yavsc/src/Startup.cs @@ -10,6 +10,7 @@ using System.Web.Optimization; using AspNet.Security.OpenIdConnect.Extensions; using Microsoft.AspNet.Authentication; using Microsoft.AspNet.Authentication.Cookies; +using Microsoft.AspNet.Authentication.JwtBearer; using Microsoft.AspNet.Authentication.OAuth; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; @@ -18,6 +19,7 @@ using Microsoft.AspNet.Diagnostics; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Localization; @@ -72,7 +74,7 @@ namespace Yavsc public class Startup { - public static string UserFilesDirName { get; private set; } + public static string UserFilesDirName { get; private set; } private RsaSecurityKey key; public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) @@ -159,6 +161,16 @@ namespace Yavsc { options.SignInScheme = "ServerCookie"; }); + services.Configure( + to => + { + to.Audience = Configuration["Site:Audience"]; + to.Issuer = Configuration["Site:Authority"]; + to.SigningCredentials = + new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature); + + } + ); services.Add(ServiceDescriptor.Singleton(typeof(IOptions), typeof(OptionsManager))); services.Add(ServiceDescriptor.Singleton(typeof(IOptions), typeof(OptionsManager))); @@ -179,11 +191,7 @@ namespace Yavsc new DirectoryInfo(Configuration["DataProtection:Keys:Dir"])); }); - services.AddAuthentication(options => - { - options.SignInScheme = "ServerCookie"; - } - ); + services.AddAuthentication(); // Add framework services. services.AddEntityFramework() .AddNpgsql() @@ -195,11 +203,15 @@ namespace Yavsc { option.User.AllowedUserNameCharacters += " "; option.User.RequireUniqueEmail = true; + option.Cookies.ApplicationCookie.LoginPath = "/authenticate"; + option.Cookies.ApplicationCookie.LogoutPath = "/signout"; + option.Cookies.ApplicationCookie.AccessDeniedPath = "/forbidden"; // TODO /forbidden + // FIXME option.Cookies.ApplicationCookie.ReturnUrlParameter = "target"; } ).AddEntityFrameworkStores() .AddTokenProvider>(Constants.EMailFactor) .AddTokenProvider(Constants.DefaultFactor) - ; + ; // .AddTokenProvider(Constants.SMSFactor) // @@ -229,14 +241,16 @@ namespace Yavsc { policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName); }); - + options.AddPolicy("FrontOffice", policy => policy.RequireRole(Constants.FrontOfficeGroupName)); options.AddPolicy("API", policy => { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireClaim(OpenIdConnectConstants.Claims.Scope, "api-resource-controller"); }); // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); + options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); }); services.AddSingleton(); @@ -341,6 +355,7 @@ namespace Yavsc } } + var googleOptions = new GoogleOptions { ClientId = Configuration["Authentication:Google:ClientId"], @@ -370,14 +385,54 @@ namespace Yavsc googleOptions.Scope.Add("https://www.googleapis.com/auth/calendar"); - app.UseIISPlatformHandler(options => options.AuthenticationDescriptions.Clear()); + app.UseIISPlatformHandler(options => + { + options.AuthenticationDescriptions.Clear(); + options.AutomaticAuthentication = true; + }); + app.UseFileServer(new FileServerOptions() + { + FileProvider = new PhysicalFileProvider( + Path.Combine( + env.WebRootPath, + // TODO: add a ressource serveur id here, + // or remove the blog entry id usage, to use the userid instead + // and an user defined optional subdir. + siteSettings.Value.UserFiles.DirName + )), + RequestPath = new PathString("/" + siteSettings.Value.UserFiles.DirName), + EnableDirectoryBrowsing = false + }); app.UseStaticFiles().UseWebSockets(); - - app.UseRequestLocalization(localizationOptions.Value, (RequestCulture)new RequestCulture((string)"fr")); - app.UseIdentity(); + 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"); */ + }); + app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch => { branch.UseJwtBearerAuthentication(options => @@ -401,7 +456,7 @@ namespace Yavsc options.AuthenticationScheme = "ServerCookie"; options.CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie"; options.ExpireTimeSpan = TimeSpan.FromMinutes(5); - options.LoginPath = new PathString("/signin"); + options.LoginPath = new PathString("/authenticate"); options.LogoutPath = new PathString("/signout"); }); @@ -417,45 +472,10 @@ namespace Yavsc }); }); - var authProvider = new AuthorizationProvider(loggerFactory); + - app.UseOpenIdConnectServer(options => - { - options.Provider = authProvider; - - // 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"); */ - }); - - app.UseFileServer(new FileServerOptions() - { - FileProvider = new PhysicalFileProvider( - Path.Combine( - env.WebRootPath, - siteSettings.Value.UserFiles.DirName - )), - RequestPath = new PathString("/"+siteSettings.Value.UserFiles.DirName), - EnableDirectoryBrowsing = false - }); + app.UseRequestLocalization(localizationOptions.Value, (RequestCulture)new RequestCulture((string)"fr")); /* Generic OAuth (here GitHub): options.Notifications = new OAuthAuthenticationNotifications { @@ -501,12 +521,12 @@ namespace Yavsc } }; */ + + + + app.UseMvc(routes => { - /* routes.MapRoute( - name: "wf", - template: "do/{action=Index}/{id?}", - defaults: new { controller = "WorkFlow" }); */ routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); diff --git a/Yavsc/wwwroot/css/site.css b/Yavsc/wwwroot/css/site.css index fcd3f4f4..65a2cedb 100755 --- a/Yavsc/wwwroot/css/site.css +++ b/Yavsc/wwwroot/css/site.css @@ -13,9 +13,14 @@ textarea { max-width: 280px; } -.navbar-reac { - +/* Set widths on image and video, since otherwise they use their native resolution */ + +img, +video { + max-width: 100%; } +/* .navbar-reac */ + /* Carousel */ .carousel-caption p { font-family: "jubilat";