From ff65d1520e68300a6612d3322868b716c2ad8355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 1 Mar 2018 15:46:47 +0300 Subject: [PATCH] Aded consent page for IDS. --- Volo.Abp.sln | 4 +- .../Pages/Account/Login.cshtml.cs | 1 + .../Pages/Account/_ScopeListItem.cshtml | 38 +++ .../Pages/Consent.cshtml | 76 ++++++ .../Pages/Consent.cshtml.cs | 218 ++++++++++++++++++ .../Pages/_ViewImports.cshtml | 3 + ...Volo.Abp.Account.Web.IdentityServer.csproj | 4 + 7 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/_ScopeListItem.cshtml create mode 100644 src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml create mode 100644 src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs create mode 100644 src/Volo.Abp.Account.Web.IdentityServer/Pages/_ViewImports.cshtml diff --git a/Volo.Abp.sln b/Volo.Abp.sln index ee8cece3d9..f414740370 100644 --- a/Volo.Abp.sln +++ b/Volo.Abp.sln @@ -296,9 +296,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroserviceDemo.TenancySer EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MultiTenancy.HttpApi.Client", "src\Volo.Abp.MultiTenancy.HttpApi.Client\Volo.Abp.MultiTenancy.HttpApi.Client.csproj", "{76D24E2C-8DB0-48B7-9FC4-02231B8B9F39}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroserviceDemo.AuthServer", "src\MicroserviceDemo\MicroserviceDemo.AuthServer\MicroserviceDemo.AuthServer.csproj", "{A177B8B7-ACAD-4F75-B6D4-B72F7BC2E40A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroserviceDemo.AuthServer", "src\MicroserviceDemo\MicroserviceDemo.AuthServer\MicroserviceDemo.AuthServer.csproj", "{A177B8B7-ACAD-4F75-B6D4-B72F7BC2E40A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Account.Web.IdentityServer", "src\Volo.Abp.Account.Web.IdentityServer\Volo.Abp.Account.Web.IdentityServer.csproj", "{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Account.Web.IdentityServer", "src\Volo.Abp.Account.Web.IdentityServer\Volo.Abp.Account.Web.IdentityServer.csproj", "{E4AB8A4F-BB59-4BDB-B915-877CE97D8113}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/Login.cshtml.cs b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/Login.cshtml.cs index 990e168229..d7c95c98e5 100644 --- a/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/Login.cshtml.cs +++ b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/Login.cshtml.cs @@ -23,6 +23,7 @@ using Volo.Abp.Uow; namespace Volo.Abp.Account.Web.Pages.Account { + //TODO: Inherit from LoginModel of Account.Web project. We should design it as extensible. public class IdsLoginModel : AccountModelBase { [HiddenInput] diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/_ScopeListItem.cshtml b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/_ScopeListItem.cshtml new file mode 100644 index 0000000000..12d6ae8c01 --- /dev/null +++ b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/_ScopeListItem.cshtml @@ -0,0 +1,38 @@ +@using Volo.Abp.Account.Web.Pages +@using Volo.Abp.Account.Web.Pages.Account +@model Volo.Abp.Account.Web.Pages.ConsentModel.ScopeViewModel + +@* TODO: Should re-format this, just made copy/paste *@ + +
  • + + @if (Model.Required) + { + (required) + } + @if (Model.Description != null) + { + + } +
  • \ No newline at end of file diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml new file mode 100644 index 0000000000..fe23d661f8 --- /dev/null +++ b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml @@ -0,0 +1,76 @@ +@page +@using Volo.Abp.Account.Web.Pages +@using Volo.Abp.Account.Web.Pages.Account +@model ConsentModel + + +
    +
    +

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

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

    Personal Information

    + +
      + @foreach (var scope in Model.IdentityScopes) + { + @Html.Partial("Account/_ScopeListItem", scope) + } +
    + } + + @if (Model.ResourceScopes.Any()) + { +

    Application Access

    + +
      + @foreach (var scope in Model.ResourceScopes) + { + @Html.Partial("Account/_ScopeListItem", scope) + } +
    + } + + @if (Model.AllowRememberConsent) + { +
    + +
    + } + +
    + + + @if (Model.ClientUrl != null) + { + + @Model.ClientName + + } +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs new file mode 100644 index 0000000000..bd6c18b096 --- /dev/null +++ b/src/Volo.Abp.Account.Web.IdentityServer/Pages/Consent.cshtml.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +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.RazorPages; + +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 ConsentInputModel ConsentInput { get; set; } + + public string ClientName { get; set; } + public string ClientUrl { get; set; } + public string ClientLogoUrl { get; set; } + public bool AllowRememberConsent { get; set; } + + public List IdentityScopes { get; set; } + + public List ResourceScopes { 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 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)}"); + } + + ConsentInput = new ConsentInputModel + { + RememberConsent = true, + ScopesConsented = new List() + }; + + ClientName = client.ClientId; //TODO: Consider to create a ClientInfoModel + ClientUrl = client.ClientUri; + ClientLogoUrl = client.LogoUri; + AllowRememberConsent = client.AllowRememberConsent; + + IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, true)).ToList(); + ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, true)).ToList(); + + if (resources.OfflineAccess) + { + ResourceScopes = ResourceScopes.Union(new[] {GetOfflineAccessScope(true)}).ToList(); + } + } + + public 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: "); + } + + private async Task ProcessConsentAsync() + { + var result = new ProcessConsentResult(); + + ConsentResponse grantedConsent = null; + + if (ConsentInput.UserDecision == "no") + { + grantedConsent = ConsentResponse.Denied; + } + else + { + if (ConsentInput.ScopesConsented != null && ConsentInput.ScopesConsented.Any()) + { + var scopes = ConsentInput.ScopesConsented; + + grantedConsent = new ConsentResponse + { + RememberConsent = ConsentInput.RememberConsent, + ScopesConsented = scopes.ToArray() + }; + } + else + { + result.ValidationError = "You must pick at least one permission"; + } + } + + if (grantedConsent != null) + { + // validate return url is still valid + var request = await _interaction.GetAuthorizationContextAsync(ReturnUrl); + if (request == null) return result; + + // communicate outcome of consent back to identityserver + await _interaction.GrantConsentAsync(request, grantedConsent); + + // indicate that's it ok to redirect back to authorization endpoint + result.RedirectUri = ReturnUrl; //TODO: ReturnUrlHash? + } + + return result; + } + + + private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) + { + return new ScopeViewModel + { + Name = identity.Name, + DisplayName = identity.DisplayName, + Description = identity.Description, + Emphasize = identity.Emphasize, + Required = identity.Required, + Checked = check || identity.Required + }; + } + + public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) + { + return new ScopeViewModel + { + Name = scope.Name, + DisplayName = scope.DisplayName, + Description = scope.Description, + Emphasize = scope.Emphasize, + Required = scope.Required, + Checked = check || scope.Required + }; + } + + private ScopeViewModel GetOfflineAccessScope(bool check) + { + return new 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 string UserDecision { get; set; } + + public List ScopesConsented { get; set; } + + public bool RememberConsent { get; set; } + } + + public class ScopeViewModel + { + public string Name { get; set; } + public string DisplayName { get; set; } + public string Description { get; set; } + public bool Emphasize { get; set; } + public bool Required { get; set; } + public bool Checked { 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; } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Pages/_ViewImports.cshtml b/src/Volo.Abp.Account.Web.IdentityServer/Pages/_ViewImports.cshtml new file mode 100644 index 0000000000..05e1baf060 --- /dev/null +++ b/src/Volo.Abp.Account.Web.IdentityServer/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap \ No newline at end of file diff --git a/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj b/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj index f5b527a464..26958e48e0 100644 --- a/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj +++ b/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj @@ -19,6 +19,10 @@ + + + +