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
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+