Browse Source

Refactored the consensus page.

pull/4578/head
maliming 6 years ago
parent
commit
b839f67932
  1. 17
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/ConsentOptions.cs
  2. 9
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs
  3. 72
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml
  4. 252
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs
  5. 2
      modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs

17
modules/account/src/Volo.Abp.Account.Web.IdentityServer/ConsentOptions.cs

@ -0,0 +1,17 @@
namespace Volo.Abp.Account.Web
{
public class ConsentOptions
{
public static bool EnableOfflineAccess = true;
public static string OfflineAccessDisplayName = "Offline Access";
public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
//TODO: How to handle this
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
//TODO: How to handle this
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
}
}

9
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs

@ -73,9 +73,9 @@ namespace Volo.Abp.Account.Web.Pages.Account
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin); EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (context?.ClientId != null) if (context?.Client?.ClientId != null)
{ {
var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId); var client = await ClientStore.FindEnabledClientByIdAsync(context?.Client?.ClientId);
if (client != null) if (client != null)
{ {
EnableLocalLogin = client.EnableLocalLogin; EnableLocalLogin = client.EnableLocalLogin;
@ -105,7 +105,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Redirect("~/"); return Redirect("~/");
} }
await Interaction.GrantConsentAsync(context, ConsentResponse.Denied); await Interaction.GrantConsentAsync(context, new ConsentResponse()
{
Error = AuthorizationError.AccessDenied
});
return Redirect(ReturnUrl); return Redirect(ReturnUrl);
} }

72
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml

@ -1,18 +1,18 @@
@page @page
@using IdentityServer4.Extensions
@using Volo.Abp.Account.Web.Pages @using Volo.Abp.Account.Web.Pages
@using Volo.Abp.Account.Web.Pages.Account
@model ConsentModel @model ConsentModel
<abp-card id="IdentityServerConsentWrapper"> <abp-card id="IdentityServerConsentWrapper">
<abp-card-header> <abp-card-header>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h2> <h2>
@if (Model.ClientInfo.ClientLogoUrl != null) @if (Model.Consent.ClientLogoUrl != null)
{ {
<img src="@Model.ClientInfo.ClientLogoUrl"> <img src="@Model.Consent.ClientLogoUrl">
} }
@Model.ClientInfo.ClientName @Model.Consent.ClientName
<small>is requesting your permission</small> <small>is requesting your permission</small>
</h2> </h2>
</div> </div>
@ -25,29 +25,30 @@
<div>Uncheck the permissions you do not wish to grant.</div> <div>Uncheck the permissions you do not wish to grant.</div>
@if (!Model.ConsentInput.IdentityScopes.IsNullOrEmpty()) @if (!Model.Consent.IdentityScopes.IsNullOrEmpty())
{ {
<h3>Personal Information</h3> <h3>Personal Information</h3>
<ul class="list-group"> <ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++)
@foreach (var identityScope in Model.Consent.IdentityScopes)
{ {
<li class="list-group-item"> <li class="list-group-item">
<div class="form-check"> <div class="form-check">
<label asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-label"> <label asp-for="@identityScope.Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Checked" class="form-check-input" /> <input asp-for="@identityScope.Checked" class="form-check-input"/>
@Model.ConsentInput.IdentityScopes[i].DisplayName @identityScope.DisplayName
@if (Model.ConsentInput.IdentityScopes[i].Required) @if (identityScope.Required)
{ {
<span><em>(required)</em></span> <span><em>(required)</em></span>
} }
</label> </label>
</div> </div>
<input asp-for="@Model.ConsentInput.IdentityScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@ <input asp-for="@identityScope.Value" type="hidden"/> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.IdentityScopes[i].Description != null) @if (identityScope.Description != null)
{ {
<div class="consent-description"> <div class="consent-description">
@Model.ConsentInput.IdentityScopes[i].Description @identityScope.Description
</div> </div>
} }
</li> </li>
@ -55,29 +56,30 @@
</ul> </ul>
} }
@if (!Model.ConsentInput.ApiScopes.IsNullOrEmpty()) @if (!Model.Consent.ApiScopes.IsNullOrEmpty())
{ {
<h3>Application Access</h3> <h3>Application Access</h3>
<ul class="list-group"> <ul class="list-group">
@for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++)
@foreach (var apiScope in Model.Consent.ApiScopes)
{ {
<li class="list-group-item"> <li class="list-group-item">
<div class="form-check"> <div class="form-check">
<label asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-label"> <label asp-for="@apiScope.Checked" class="form-check-label">
<input asp-for="@Model.ConsentInput.ApiScopes[i].Checked" class="form-check-input" disabled="@Model.ConsentInput.ApiScopes[i].Required" /> <input asp-for="@apiScope.Checked" class="form-check-input" disabled="@apiScope.Required"/>
@Model.ConsentInput.ApiScopes[i].DisplayName @apiScope.DisplayName
@if (Model.ConsentInput.ApiScopes[i].Required) @if (apiScope.Required)
{ {
<span><em>(required)</em></span> <span><em>(required)</em></span>
} }
</label> </label>
</div> </div>
<input asp-for="@Model.ConsentInput.ApiScopes[i].Name" type="hidden" /> @* TODO: Use attributes on the view model instead of using hidden here *@ <input asp-for="@apiScope.Value" type="hidden"/> @* TODO: Use attributes on the view model instead of using hidden here *@
@if (Model.ConsentInput.ApiScopes[i].Description != null) @if (apiScope.Description != null)
{ {
<div class="consent-description"> <div class="consent-description">
@Model.ConsentInput.ApiScopes[i].Description @apiScope.Description
</div> </div>
} }
</li> </li>
@ -85,11 +87,23 @@
</ul> </ul>
} }
@if (Model.ClientInfo.AllowRememberConsent) <div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-tasks"></span>
Description
</div>
<div class="card-body">
<input class="form-control" placeholder="Description or name of device" asp-for="@Model.Consent.Description" autofocus>
</div>
</div>
</div>
@if (Model.Consent.AllowRememberConsent)
{ {
<div class="form-check"> <div class="form-check">
<label asp-for="@Model.ConsentInput.RememberConsent" class="form-check-label"> <label asp-for="@Model.Consent.RememberConsent" class="form-check-label">
<input asp-for="@Model.ConsentInput.RememberConsent" class="form-check-input" /> <input asp-for="@Model.Consent.RememberConsent" class="form-check-input" />
<strong>Remember My Decision</strong> <strong>Remember My Decision</strong>
</label> </label>
</div> </div>
@ -98,10 +112,10 @@
<div> <div>
<button name="UserDecision" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button> <button name="UserDecision" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="UserDecision" value="no" class="btn">No, Do Not Allow</button> <button name="UserDecision" value="no" class="btn">No, Do Not Allow</button>
@if (Model.ClientInfo.ClientUrl != null) @if (Model.Consent.ClientUrl != null)
{ {
<a class="pull-right btn btn-secondary" target="_blank" href="@Model.ClientInfo.ClientUrl"> <a class="pull-right btn btn-secondary" target="_blank" href="@Model.Consent.ClientUrl">
<strong>@Model.ClientInfo.ClientName</strong> <strong>@Model.Consent.ClientName</strong>
</a> </a>
} }
</div> </div>
@ -110,4 +124,4 @@
</form> </form>
</abp-card-body> </abp-card-body>
</abp-card> </abp-card>

252
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs

@ -3,12 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4.Extensions;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.UI;
namespace Volo.Abp.Account.Web.Pages namespace Volo.Abp.Account.Web.Pages
{ {
@ -24,9 +25,7 @@ namespace Volo.Abp.Account.Web.Pages
public string ReturnUrlHash { get; set; } public string ReturnUrlHash { get; set; }
[BindProperty] [BindProperty]
public ConsentModel.ConsentInputModel ConsentInput { get; set; } public ConsentViewModel Consent { get; set; }
public ClientInfoModel ClientInfo { get; set; }
private readonly IIdentityServerInteractionService _interaction; private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore; private readonly IClientStore _clientStore;
@ -44,37 +43,7 @@ namespace Volo.Abp.Account.Web.Pages
public virtual async Task<IActionResult> OnGet() public virtual async Task<IActionResult> OnGet()
{ {
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl); Consent = await BuildViewModelAsync(ReturnUrl);
if (request == null)
{
throw new ApplicationException($"No consent request matching request: {ReturnUrl}");
}
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
if (client == null)
{
throw new ApplicationException($"Invalid client id: {request.ClientId}");
}
var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);
if (resources == null || (!resources.IdentityResources.Any() && !resources.ApiResources.Any()))
{
throw new ApplicationException($"No scopes matching: {request.ScopesRequested.Aggregate((x, y) => x + ", " + y)}");
}
ClientInfo = new ClientInfoModel(client);
ConsentInput = new ConsentInputModel
{
RememberConsent = true,
IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, true)).ToList(),
ApiScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, true)).ToList()
};
if (resources.OfflineAccess)
{
ConsentInput.ApiScopes.Add(GetOfflineAccessScope(true));
}
return Page(); return Page();
} }
@ -96,53 +65,137 @@ namespace Volo.Abp.Account.Web.Pages
throw new ApplicationException("Unknown Error!"); throw new ApplicationException("Unknown Error!");
} }
protected virtual async Task<ConsentModel.ProcessConsentResult> ProcessConsentAsync() protected virtual async Task<ProcessConsentResult> ProcessConsentAsync()
{ {
var result = new ConsentModel.ProcessConsentResult(); var result = new ProcessConsentResult();
// validate return url is still valid
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl);
if (request == null)
{
return result;
}
ConsentResponse grantedConsent; ConsentResponse grantedConsent = null;
if (ConsentInput.UserDecision == "no") // user clicked 'no' - send back the standard 'access_denied' response
if (Consent?.Button == "no")
{ {
grantedConsent = ConsentResponse.Denied; grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
// emit event
//await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
} }
else // user clicked 'yes' - validate the data
else if (Consent?.Button == "yes")
{ {
if (!ConsentInput.IdentityScopes.IsNullOrEmpty() || !ConsentInput.ApiScopes.IsNullOrEmpty()) // if the user consented to some scope, build the response model
if (!Consent.ScopesConsented.IsNullOrEmpty())
{ {
var scopes = Consent.ScopesConsented;
if (ConsentOptions.EnableOfflineAccess == false)
{
scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess);
}
grantedConsent = new ConsentResponse grantedConsent = new ConsentResponse
{ {
RememberConsent = ConsentInput.RememberConsent, RememberConsent = Consent.RememberConsent,
ScopesConsented = ConsentInput.GetAllowedScopeNames() ScopesValuesConsented = scopes.ToArray(),
Description = Consent.Description
}; };
// emit event
//await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
} }
else else
{ {
throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this //throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this
result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;
} }
} }
else
{
result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;
}
if (grantedConsent != null) if (grantedConsent != null)
{ {
var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl); // communicate outcome of consent back to identityserver
if (request == null)
{
return result;
}
await _interaction.GrantConsentAsync(request, grantedConsent); await _interaction.GrantConsentAsync(request, grantedConsent);
result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash? // indicate that's it ok to redirect back to authorization endpoint
result.RedirectUri = Consent.ReturnUrl; //TODO: ReturnUrlHash?
result.Client = request.Client;
}
else
{
// we need to redisplay the consent UI
result.ViewModel = await BuildViewModelAsync(ReturnUrl, Consent);
} }
return result; return result;
} }
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) private async Task<ConsentViewModel> BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)
{
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (request != null)
{
return CreateConsentViewModel(model, returnUrl, request);
}
throw new ApplicationException($"No consent request matching request: {returnUrl}");
}
private ConsentViewModel CreateConsentViewModel(ConsentInputModel model, string returnUrl, AuthorizationRequest request)
{
var consentViewModel = new ConsentViewModel
{
RememberConsent = model?.RememberConsent ?? true,
ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),
Description = model?.Description,
ReturnUrl = returnUrl,
ClientName = request.Client.ClientName ?? request.Client.ClientId,
ClientUrl = request.Client.ClientUri,
ClientLogoUrl = request.Client.LogoUri,
AllowRememberConsent = request.Client.AllowRememberConsent
};
consentViewModel.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x =>
CreateScopeViewModel(x, consentViewModel.ScopesConsented.Contains(x.Name) || model == null))
.ToArray();
var apiScopes = new List<ScopeViewModel>();
foreach(var parsedScope in request.ValidatedResources.ParsedScopes)
{
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
if (apiScope != null)
{
var scopeVm = CreateScopeViewModel(parsedScope, apiScope,
consentViewModel.ScopesConsented.Contains(parsedScope.RawValue) || model == null);
apiScopes.Add(scopeVm);
}
}
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
{
apiScopes.Add(GetOfflineAccessScope(consentViewModel.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));
}
consentViewModel.ApiScopes = apiScopes;
return consentViewModel;
}
protected virtual ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{ {
return new ConsentModel.ScopeViewModel return new ScopeViewModel
{ {
Name = identity.Name, Value = identity.Name,
DisplayName = identity.DisplayName, DisplayName = identity.DisplayName,
Description = identity.Description, Description = identity.Description,
Emphasize = identity.Emphasize, Emphasize = identity.Emphasize,
@ -151,24 +204,30 @@ namespace Volo.Abp.Account.Web.Pages
}; };
} }
protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(Scope scope, bool check) protected virtual ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
{ {
return new ConsentModel.ScopeViewModel var displayName = apiScope.DisplayName ?? apiScope.Name;
{ if (!string.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
Name = scope.Name, {
DisplayName = scope.DisplayName, displayName += ":" + parsedScopeValue.ParsedParameter;
Description = scope.Description, }
Emphasize = scope.Emphasize,
Required = scope.Required, return new ScopeViewModel
Checked = check || scope.Required {
Value = parsedScopeValue.RawValue,
DisplayName = displayName,
Description = apiScope.Description,
Emphasize = apiScope.Emphasize,
Required = apiScope.Required,
Checked = check || apiScope.Required
}; };
} }
protected virtual ConsentModel.ScopeViewModel GetOfflineAccessScope(bool check) protected virtual ScopeViewModel GetOfflineAccessScope(bool check)
{ {
return new ConsentModel.ScopeViewModel return new ScopeViewModel
{ {
Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, Value = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = "Offline Access", //TODO: Localize DisplayName = "Offline Access", //TODO: Localize
Description = "Access to your applications and resources, even when you are offline", Description = "Access to your applications and resources, even when you are offline",
Emphasize = true, Emphasize = true,
@ -178,28 +237,37 @@ namespace Volo.Abp.Account.Web.Pages
public class ConsentInputModel public class ConsentInputModel
{ {
public List<ConsentModel.ScopeViewModel> IdentityScopes { get; set; }
public List<ConsentModel.ScopeViewModel> ApiScopes { get; set; }
[Required] [Required]
public string UserDecision { get; set; } public string Button { get; set; }
public IEnumerable<string> ScopesConsented { get; set; }
public bool RememberConsent { get; set; } public bool RememberConsent { get; set; }
public List<string> GetAllowedScopeNames() public string ReturnUrl { get; set; }
{
var identityScopes = IdentityScopes ?? new List<ConsentModel.ScopeViewModel>(); public string Description { get; set; }
var apiScopes = ApiScopes ?? new List<ConsentModel.ScopeViewModel>(); }
return identityScopes.Union(apiScopes).Where(s => s.Checked).Select(s => s.Name).ToList();
}
public class ConsentViewModel : ConsentInputModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }
public IEnumerable<ScopeViewModel> ApiScopes { get; set; }
} }
public class ScopeViewModel public class ScopeViewModel
{ {
[Required] [Required]
[HiddenInput] [HiddenInput]
public string Name { get; set; } public string Value { get; set; }
public bool Checked { get; set; } public bool Checked { get; set; }
@ -216,29 +284,13 @@ namespace Volo.Abp.Account.Web.Pages
{ {
public bool IsRedirect => RedirectUri != null; public bool IsRedirect => RedirectUri != null;
public string RedirectUri { get; set; } public string RedirectUri { get; set; }
public Client Client { get; set; }
public bool ShowView => ViewModel != null;
public ConsentViewModel ViewModel { get; set; }
public bool HasValidationError => ValidationError != null; public bool HasValidationError => ValidationError != null;
public string ValidationError { get; set; } public string ValidationError { get; set; }
} }
public class ClientInfoModel
{
public string ClientName { get; set; }
public string ClientUrl { get; set; }
public string ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public ClientInfoModel(Client client)
{
//TODO: Automap
ClientName = client.ClientId;
ClientUrl = client.ClientUri;
ClientLogoUrl = client.LogoUri;
AllowRememberConsent = client.AllowRememberConsent;
}
}
} }
} }

2
modules/identityserver/test/Volo.Abp.IdentityServer.Domain.Tests/Volo/Abp/IdentityServer/Clients/IdentityResourceStore_Tests.cs

@ -53,7 +53,7 @@ namespace Volo.Abp.IdentityServer.Clients
//Assert //Assert
apiResources.ShouldNotBe(null); apiResources.ShouldNotBe(null);
apiResources[0].Scopes.Count.ShouldBe(2); apiResources[0].Scopes.Count.ShouldBe(3);
} }
[Fact] [Fact]

Loading…
Cancel
Save