Browse Source

Consent screen.

pull/242/head
Sebastian Stehle 9 years ago
parent
commit
57b0416ace
  1. 2
      src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  2. 51
      src/Squidex.Domain.Users/UserExtensions.cs
  3. 2
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  4. 32
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  5. 4
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  6. 6
      src/Squidex.Shared/Users/IUser.cs
  7. 100
      src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  8. 18
      src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs
  9. 16
      src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs
  10. 56
      src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  11. 91
      src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml
  12. 4
      src/Squidex/Config/Authentication/GoogleHandler.cs
  13. 4
      src/Squidex/Config/Authentication/MicrosoftHandler.cs
  14. 2
      src/Squidex/Config/MyIdentityOptions.cs
  15. 5
      src/Squidex/Squidex.csproj
  16. 18
      src/Squidex/app/theme/_static.scss
  17. 6
      src/Squidex/appsettings.json

2
src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Users.MongoDb
Id = ObjectId.GenerateNewId().ToString();
}
public void UpdateEmail(string email)
public void SetEmail(string email)
{
Email = UserName = email;
}

51
src/Squidex.Domain.Users/UserExtensions.cs

@ -35,19 +35,64 @@ namespace Squidex.Domain.Users
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
}
public static void SetConsent(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
}
public static void SetConsentForEmails(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
}
public static bool HasConsent(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");
}
public static bool HasConsentForEmails(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsentForEmails, "true");
}
public static bool HasDisplayName(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexDisplayName);
}
public static bool HasPictureUrl(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexPictureUrl);
}
public static bool IsPictureUrlStored(this IUser user)
{
return string.Equals(user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value, "store", StringComparison.OrdinalIgnoreCase);
return user.HasClaimValue(SquidexClaimTypes.SquidexPictureUrl, "store");
}
public static string PictureUrl(this IUser user)
{
return user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value;
return user.GetClaimValue(SquidexClaimTypes.SquidexPictureUrl);
}
public static string DisplayName(this IUser user)
{
return user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexDisplayName)?.Value;
return user.GetClaimValue(SquidexClaimTypes.SquidexDisplayName);
}
public static string GetClaimValue(this IUser user, string claim)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase))?.Value;
}
public static bool HasClaim(this IUser user, string claim)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase));
}
public static bool HasClaimValue(this IUser user, string claim, string value)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase));
}
public static string PictureNormalizedUrl(this IUser user)

2
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -73,7 +73,7 @@ namespace Squidex.Domain.Users
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName)
{
user.UpdateEmail(email);
user.SetEmail(email);
user.SetDisplayName(displayName);
return userManager.UpdateAsync(user);

32
src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace Squidex.Shared.Identity
{
public static class ClaimsPrincipalExtensions
{
public static void SetDisplayName(this ClaimsIdentity identity, string displayName)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName));
}
public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
}
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal));
}
}
}

4
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -13,6 +13,10 @@ namespace Squidex.Shared.Identity
public static readonly string SquidexPictureUrl = "urn:squidex:picture";
public static readonly string SquidexConsent = "urn:squidex:consent";
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string Prefix = "urn:squidex:";
}
}

6
src/Squidex.Shared/Users/IUser.cs

@ -24,10 +24,10 @@ namespace Squidex.Shared.Users
IReadOnlyList<ExternalLogin> Logins { get; }
void UpdateEmail(string email);
void AddClaim(Claim claim);
void SetEmail(string email);
void SetClaim(string type, string value);
void AddClaim(Claim claim);
}
}

100
src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -12,6 +12,7 @@ using System.Security;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -88,21 +89,52 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
}
[HttpGet]
[Route("account/logout/")]
public async Task<IActionResult> Logout(string logoutId)
[Route("account/consent/")]
public IActionResult Consent(string returnUrl = null)
{
var context = await interactions.GetLogoutContextAsync(logoutId);
return View(new ConsentVM { PrivacyUrl = identityOptions.Value.PrivacyUrl, ReturnUrl = returnUrl });
}
await signInManager.SignOutAsync();
[HttpPost]
[Route("account/consent/")]
public async Task<IActionResult> Consent(ConsentModel model, string returnUrl = null)
{
if (!model.ConsentToCookies)
{
ModelState.AddModelError(nameof(model.ConsentToCookies), "You have to give consent.");
}
var logoutUrl = context.PostLogoutRedirectUri;
if (!model.ConsentToPersonalInformation)
{
ModelState.AddModelError(nameof(model.ConsentToPersonalInformation), "You have to give consent.");
}
if (string.IsNullOrWhiteSpace(logoutUrl))
if (!ModelState.IsValid)
{
logoutUrl = urlOptions.Value.BuildUrl("logout/");
var vm = new ConsentVM { PrivacyUrl = identityOptions.Value.PrivacyUrl, ReturnUrl = returnUrl };
return View(vm);
}
return Redirect(logoutUrl);
var user = await userManager.GetUserAsync(User);
user.SetConsentForEmails(model.ConsentToAutomatedEmails);
user.SetConsent();
await userManager.UpdateAsync(user);
return RedirectToReturnUrl(returnUrl);
}
[HttpGet]
[Route("account/logout/")]
public async Task<IActionResult> Logout(string logoutId)
{
var context = await interactions.GetLogoutContextAsync(logoutId);
await signInManager.SignOutAsync();
return RedirectToLogoutUrl(context);
}
[HttpGet]
@ -143,13 +175,9 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
return await LoginViewAsync(returnUrl, true, true);
}
else if (!string.IsNullOrWhiteSpace(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return Redirect("~/../");
return RedirectToReturnUrl(returnUrl);
}
}
@ -203,11 +231,13 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
var isLoggedIn = result.Succeeded;
IUser user = null;
if (!isLoggedIn)
{
var email = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
var user = await userManager.FindByEmailAsync(email);
user = await userManager.FindByEmailAsync(email);
if (user != null)
{
@ -241,13 +271,13 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
return RedirectToAction(nameof(Login));
}
else if (!string.IsNullOrWhiteSpace(returnUrl))
else if (user != null && !user.HasConsent())
{
return Redirect(returnUrl);
return RedirectToAction(nameof(Consent), new { returnUrl });
}
else
{
return Redirect("~/../");
return RedirectToReturnUrl(returnUrl);
}
}
@ -292,24 +322,48 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
{
var user = userFactory.Create(email);
if (!externalLogin.Principal.HasClaim(x => x.Type == SquidexClaimTypes.SquidexPictureUrl))
foreach (var squidexClaim in externalLogin.Principal.GetSquidexClaims())
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
user.AddClaim(squidexClaim);
}
if (!externalLogin.Principal.HasClaim(x => x.Type == SquidexClaimTypes.SquidexDisplayName))
if (!user.HasPictureUrl())
{
user.SetClaim(SquidexClaimTypes.SquidexDisplayName, email);
user.SetPictureUrl(GravatarHelper.CreatePictureUrl(email));
}
foreach (var squidexClaim in externalLogin.Principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal)))
if (!user.HasDisplayName())
{
user.AddClaim(squidexClaim);
user.SetDisplayName(email);
}
return user;
}
private IActionResult RedirectToLogoutUrl(LogoutRequest context)
{
if (!string.IsNullOrWhiteSpace(context.PostLogoutRedirectUri))
{
return Redirect(context.PostLogoutRedirectUri);
}
else
{
return Redirect("~/../");
}
}
private IActionResult RedirectToReturnUrl(string returnUrl)
{
if (!string.IsNullOrWhiteSpace(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return Redirect("~/../");
}
}
private async Task<bool> MakeIdentityOperation(Func<Task<IdentityResult>> action, [CallerMemberName] string operationName = null)
{
try

18
src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentModel.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.IdentityServer.Controllers.Account
{
public sealed class ConsentModel
{
public bool ConsentToPersonalInformation { get; set; }
public bool ConsentToAutomatedEmails { get; set; }
public bool ConsentToCookies { get; set; }
}
}

16
src/Squidex/Areas/IdentityServer/Controllers/Account/ConsentVM.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.IdentityServer.Controllers.Account
{
public sealed class ConsentVM
{
public string ReturnUrl { get; set; }
public string PrivacyUrl { get; set; }
}
}

56
src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -75,12 +75,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/login-add-callback/")]
public Task<IActionResult> AddLoginCallback()
{
return MakeChangeAsync(async user =>
{
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User));
return await userManager.AddLoginAsync(user, externalLogin);
}, "Login added successfully.");
return MakeChangeAsync(user => AddLoginAsync(user),
"Login added successfully.");
}
[HttpPost]
@ -119,31 +115,41 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile
[Route("/account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> file)
{
return MakeChangeAsync(async user =>
return MakeChangeAsync(user => UpdatePictureAsync(file, user),
"Picture uploaded successfully.");
}
private async Task<IdentityResult> AddLoginAsync(IUser user)
{
var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User));
return await userManager.AddLoginAsync(user, externalLogin);
}
private async Task<IdentityResult> UpdatePictureAsync(List<IFormFile> file, IUser user)
{
if (file.Count != 1)
{
if (file.Count != 1)
{
return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." });
}
return IdentityResult.Failed(new IdentityError { Description = "Please upload a single file." });
}
var thumbnailStream = new MemoryStream();
try
{
await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop");
var thumbnailStream = new MemoryStream();
try
{
await assetThumbnailGenerator.CreateThumbnailAsync(file[0].OpenReadStream(), thumbnailStream, 128, 128, "Crop");
thumbnailStream.Position = 0;
}
catch
{
return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." });
}
thumbnailStream.Position = 0;
}
catch
{
return IdentityResult.Failed(new IdentityError { Description = "Picture is not a valid image." });
}
await userPictureStore.UploadAsync(user.Id, thumbnailStream);
await userPictureStore.UploadAsync(user.Id, thumbnailStream);
user.SetPictureUrlToStore();
user.SetPictureUrlToStore();
return await userManager.UpdateAsync(user);
}, "Picture uploaded successfully.");
return await userManager.UpdateAsync(user);
}
private async Task<IActionResult> MakeChangeAsync(Func<IUser, Task<IdentityResult>> action, string successMessage, ChangeProfileModel model = null)

91
src/Squidex/Areas/IdentityServer/Views/Account/Consent.cshtml

@ -0,0 +1,91 @@
@model Squidex.Areas.IdentityServer.Controllers.Account.ConsentVM
@{
ViewBag.Theme = "white";
ViewBag.Title = "Consent";
}
@functions {
public string ErrorClass(string error)
{
return ViewData.ModelState[error]?.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid ? "border-danger" : "";
}
}
<form asp-controller="Account" asp-action="Consent" asp-route-returnurl="@Model.ReturnUrl" method="post">
<div class="profile profile-lg">
<img class="profile-logo" src="~/images/logo-small.png" />
<h2>We need your consent</h2>
<div class="card profile-section-sm @ErrorClass("ConsentToAutomatedEmails")">
<div class="card-body">
<h4 class="card-title">Automated E-Mails</h4>
<div class="card-text row">
<div class="col col-auto">
<input type="checkbox" name="consentToAutomatedEmails" value="True" />
</div>
<div class="col">
I understand and agree that Squidex sends Emails to imform me about new features, breaking changes and downtimes.
</div>
</div>
</div>
</div>
<div class="card profile-section-sm @ErrorClass("ConsentToCookies")">
<div class="card-body">
<h4 class="card-title">Cookies & Analytics</h4>
<div class="card-text row">
<div class="col col-auto">
<input type="checkbox" name="consentToCookies" value="True" />
</div>
<div class="col">
<p>
I understand and agree that Squidex uses cookies to ensure you get the best experience on our platform and to store your login status.
</p>
<p>
I understand and agree that Squidex has integrated Google Analytics (with the anonymizer function). Google Analytics is a web analytics service to gather and analyse data about the behavior of users.
</p>
<p>
I have read the <a href="@Model.PrivacyUrl" target="_blank">Privacy Policies</a>.
</p>
</div>
</div>
</div>
</div>
<div class="card profile-section-sm @ErrorClass("ConsentToPersonalInformation")">
<div class="card-body">
<h4 class="card-title">Personal Information</h4>
<div class="card-text row">
<div class="col col-auto">
<input type="checkbox" name="consentToPersonalInformation" value="True" />
</div>
<div class="col">
I understand and agree that Squidex collects the following private information that are retrieved from external authentication providers such as Google, Microsoft or Github.
<ul class="personal-information">
<li>
Basic personal information (e-mail address, name and picture) are provided to all other users so that they can add you to their working space.
</li>
<li>
At anytime you have the option to change these information to anonymize your account.
</li>
<li>
Your user account has an unique identifier and for all your changes we track, that you made these changes and provide this information to other users.
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="profile-section-sm text-right">
<button type="submit" class="btn btn-success">I agree!</button>
</div>
</div>
</form>

4
src/Squidex/Config/Authentication/GoogleHandler.cs

@ -29,7 +29,7 @@ namespace Squidex.Config.Authentication
var displayNameClaim = context.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
if (displayNameClaim != null)
{
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayNameClaim.Value));
context.Identity.SetDisplayName(displayNameClaim.Value);
}
var pictureUrl = context.User?.Value<string>("picture");
@ -46,7 +46,7 @@ namespace Squidex.Config.Authentication
if (!string.IsNullOrWhiteSpace(pictureUrl))
{
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
context.Identity.SetPictureUrl(pictureUrl);
}
return base.CreatingTicket(context);

4
src/Squidex/Config/Authentication/MicrosoftHandler.cs

@ -20,7 +20,7 @@ namespace Squidex.Config.Authentication
if (!string.IsNullOrEmpty(displayName))
{
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName));
context.Identity.SetDisplayName(displayName);
}
var id = context.User.Value<string>("id");
@ -29,7 +29,7 @@ namespace Squidex.Config.Authentication
{
var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture";
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
context.Identity.SetPictureUrl(pictureUrl);
}
return base.CreatingTicket(context);

2
src/Squidex/Config/MyIdentityOptions.cs

@ -23,6 +23,8 @@ namespace Squidex.Config
public string AuthorityUrl { get; set; }
public string PrivacyUrl { get; set; }
public bool RequiresHttps { get; set; }
public bool AllowPasswordAuth { get; set; }

5
src/Squidex/Squidex.csproj

@ -63,10 +63,15 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.2" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.4" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.3" />
<PackageReference Include="Microsoft.OData.Core" Version="7.4.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.2" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" />
<PackageReference Include="NJsonSchema" Version="9.10.19" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />

18
src/Squidex/app/theme/_static.scss

@ -44,6 +44,10 @@ noscript {
margin-top: 2rem;
}
&-section-sm {
margin-top: 1rem;
}
&-picture-col {
max-width: 7rem;
}
@ -111,6 +115,20 @@ noscript {
width: 1.6rem;
}
}
.personal-information {
margin: 1rem 0;
}
p {
& {
margin-bottom: .5rem;
}
&:last-child {
margin-bottom: 0;
}
}
}
//

6
src/Squidex/appsettings.json

@ -210,6 +210,10 @@
/*
* Lock new users automatically, the administrator must unlock them.
*/
"lockAutomatically": false
"lockAutomatically": false,
/*
* The url to you privacy statements, if you host squidex by yourself.
*/
"privacyUrl": "https://squidex.io/privacy"
}
}
Loading…
Cancel
Save