diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs index 132d7a81db..6a68ebc91a 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/AbpAccountWebIdentityServerModule.cs @@ -1,5 +1,6 @@ using Volo.Abp.IdentityServer; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.Account.Web { @@ -9,6 +10,12 @@ namespace Volo.Abp.Account.Web )] public class AbpAccountWebIdentityServerModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded("Volo.Abp.Account.Web"); + }); + } } } diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml new file mode 100644 index 0000000000..fa3efda621 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml @@ -0,0 +1,113 @@ +@page +@using Volo.Abp.Account.Web.Pages +@using Volo.Abp.Account.Web.Pages.Account +@model ConsentModel + + +
+
+

+ @if (Model.ClientInfo.ClientLogoUrl != null) + { + + } + + @Model.ClientInfo.ClientName + is requesting your permission +

+
+
+
+ +
+ + + +
Uncheck the permissions you do not wish to grant.
+ + @if (Model.ConsentInput.IdentityScopes.Any()) + { +

Personal Information

+ +
    + @for (var i = 0; i < Model.ConsentInput.IdentityScopes.Count; i++) + { +
  • +
    + +
    + @* TODO: Use attributes on the view model instead of using hidden here *@ + @if (Model.ConsentInput.IdentityScopes[i].Description != null) + { + + } +
  • + } +
+ } + + @if (Model.ConsentInput.ApiScopes.Any()) + { +

Application Access

+ +
    + @for (var i = 0; i < Model.ConsentInput.ApiScopes.Count; i++) + { +
  • +
    + +
    + @* TODO: Use attributes on the view model instead of using hidden here *@ + @if (Model.ConsentInput.ApiScopes[i].Description != null) + { + + } +
  • + } +
+ } + + @if (Model.ClientInfo.AllowRememberConsent) + { +
+ +
+ } + +
+ + + @if (Model.ClientInfo.ClientUrl != null) + { + + @Model.ClientInfo.ClientName + + } +
+ +
+ +
+
+
\ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs new file mode 100644 index 0000000000..3fb68feaca --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.UI; + +namespace Volo.Abp.Account.Web.Pages +{ + //TODO: Move this into the Account folder!!! + public class ConsentModel : AbpPageModel + { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [HiddenInput] + [BindProperty(SupportsGet = true)] + public string ReturnUrlHash { get; set; } + + [BindProperty] + public ConsentModel.ConsentInputModel ConsentInput { get; set; } + + public ClientInfoModel ClientInfo { get; set; } + + private readonly IIdentityServerInteractionService _interaction; + private readonly IClientStore _clientStore; + private readonly IResourceStore _resourceStore; + + public ConsentModel( + IIdentityServerInteractionService interaction, + IClientStore clientStore, + IResourceStore resourceStore) + { + _interaction = interaction; + _clientStore = clientStore; + _resourceStore = resourceStore; + } + + public virtual async Task OnGet() + { + var request = await _interaction.GetAuthorizationContextAsync(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)); + } + } + + public virtual async Task OnPost(string userDecision) + { + var result = await ProcessConsentAsync(); + + if (result.IsRedirect) + { + return Redirect(result.RedirectUri); + } + + if (result.HasValidationError) + { + //ModelState.AddModelError("", result.ValidationError); + throw new ApplicationException("Error: " + result.ValidationError); + } + + throw new ApplicationException("Unknown Error!"); + } + + protected virtual async Task ProcessConsentAsync() + { + var result = new ConsentModel.ProcessConsentResult(); + + ConsentResponse grantedConsent; + + if (ConsentInput.UserDecision == "no") + { + grantedConsent = ConsentResponse.Denied; + } + else + { + if (ConsentInput.IdentityScopes.Any() || ConsentInput.ApiScopes.Any()) + { + grantedConsent = new ConsentResponse + { + RememberConsent = ConsentInput.RememberConsent, + ScopesConsented = ConsentInput.GetAllowedScopeNames() + }; + } + else + { + throw new UserFriendlyException("You must pick at least one permission"); //TODO: How to handle this + } + } + + if (grantedConsent != null) + { + var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl); + if (request == null) + { + return result; + } + + await _interaction.GrantConsentAsync(request, grantedConsent); + + result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash? + } + + return result; + } + + protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ConsentModel.ScopeViewModel + { + Name = identity.Name, + DisplayName = identity.DisplayName, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + protected virtual ConsentModel.ScopeViewModel CreateScopeViewModel(Scope scope, bool check) + { + return new ConsentModel.ScopeViewModel + { + Name = scope.Name, + DisplayName = scope.DisplayName, + Description = scope.Description, + Emphasize = scope.Emphasize, + Required = scope.Required, + Checked = check || scope.Required + }; + } + + protected virtual ConsentModel.ScopeViewModel GetOfflineAccessScope(bool check) + { + return new ConsentModel.ScopeViewModel + { + Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, + DisplayName = "Offline Access", //TODO: Localize + Description = "Access to your applications and resources, even when you are offline", + Emphasize = true, + Checked = check + }; + } + + public class ConsentInputModel + { + public List IdentityScopes { get; set; } + + public List ApiScopes { get; set; } + + [Required] + public string UserDecision { get; set; } + + public bool RememberConsent { get; set; } + + public List GetAllowedScopeNames() + { + return IdentityScopes.Union(ApiScopes).Where(s => s.Checked).Select(s => s.Name).ToList(); + } + } + + public class ScopeViewModel + { + [Required] + [HiddenInput] + public string Name { get; set; } + + public bool Checked { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } + + public bool Emphasize { get; set; } + + public bool Required { get; set; } + } + + public class ProcessConsentResult + { + public bool IsRedirect => RedirectUri != null; + public string RedirectUri { get; set; } + + public bool HasValidationError => ValidationError != null; + 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; + } + } + } +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj index 6c81672b40..724ecc38ef 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj @@ -20,8 +20,10 @@ + + + -