Browse Source

初始化

黄贺彬 6 years ago
parent
commit
85a4a491b5
100 changed files with 12236 additions and 1 deletions
  1. 15 1
      HaBook.IES.sln
  2. 17 0
      src/component/HaBook.IES.AzureCloud.TableStorage/HaBook.IES.AzureCloud.TableStorage.csproj
  3. 212 0
      src/component/HaBook.IES.AzureCloud.TableStorage/Repositorys/AzureTableDBRepository.cs
  4. 34 0
      src/component/HaBook.IES.AzureCloud.TableStorage/Repositorys/IAzureTableDBRepository.cs
  5. 276 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/Clients.cs
  6. 68 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/Resources.cs
  7. 120 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/ServiceCollectionExtensions.cs
  8. 40 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/TestUsers.cs
  9. 107 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/DbContexts/ConfigurationDbContext.cs
  10. 101 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/DbContexts/PersistedGrantDbContext.cs
  11. 28 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResource.cs
  12. 13 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResourceClaim.cs
  13. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResourceProperty.cs
  14. 25 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiScope.cs
  15. 13 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiScopeClaim.cs
  16. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiSecret.cs
  17. 68 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Client.cs
  18. 18 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientClaim.cs
  19. 19 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientCorsOrigin.cs
  20. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientGrantType.cs
  21. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientIdPRestriction.cs
  22. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientPostLogoutRedirectUri.cs
  23. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientProperty.cs
  24. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientRedirectUri.cs
  25. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientScope.cs
  26. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientSecret.cs
  27. 16 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/DeviceFlowCodes.cs
  28. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityClaim.cs
  29. 29 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityResource.cs
  30. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityResourceProperty.cs
  31. 22 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/PersistedGrant.cs
  32. 15 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Property.cs
  33. 20 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Secret.cs
  34. 14 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/UserClaim.cs
  35. 113 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/IdentityServerEntityFrameworkBuilderExtensions.cs
  36. 269 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/ModelBuilderExtensions.cs
  37. 40 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/TokenCleanupHost.cs
  38. 16 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/HaBook.IES.IdentityServer.AzureTableStorage.csproj
  39. 54 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Interfaces/IConfigurationDbContext.cs
  40. 46 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Interfaces/IPersistedGrantDbContext.cs
  41. 48 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ApiResourceMapperProfile.cs
  42. 43 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ApiResourceMappers.cs
  43. 70 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ClientMapperProfile.cs
  44. 42 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ClientMappers.cs
  45. 34 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/IdentityResourceMapperProfile.cs
  46. 43 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/IdentityResourceMappers.cs
  47. 25 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/PersistedGrantMapperProfile.cs
  48. 52 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/PersistedGrantMappers.cs
  49. 33 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/NopLogger.cs
  50. 175 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/ConfigurationStoreOptions.cs
  51. 12 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/DatabaseOptions.cs
  52. 81 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/OperationalStoreOptions.cs
  53. 186 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/PersistedGrantStore.cs
  54. 48 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/TableConfiguration.cs
  55. 12 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/TokenCleanupOptions.cs
  56. 9 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/About.cshtml
  57. 18 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/About.cshtml.cs
  58. 19 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Contact.cshtml
  59. 18 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Contact.cshtml.cs
  60. 23 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Error.cshtml
  61. 23 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Error.cshtml.cs
  62. 96 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Index.cshtml
  63. 17 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Index.cshtml.cs
  64. 8 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Privacy.cshtml
  65. 16 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Privacy.cshtml.cs
  66. 41 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_CookieConsentPartial.cshtml
  67. 74 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_Layout.cshtml
  68. 18 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_ValidationScriptsPartial.cshtml
  69. 3 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/_ViewImports.cshtml
  70. 3 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/_ViewStart.cshtml
  71. 24 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Program.cs
  72. 27 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Properties/launchSettings.json
  73. 77 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Startup.cs
  74. 103 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/ClientStore.cs
  75. 296 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/DeviceFlowStore.cs
  76. 336 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/PersistedGrantStore.cs
  77. 293 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/ResourceStore.cs
  78. 23 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/IOperationalStoreNotification.cs
  79. 107 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/Services/CorsPolicyService.cs
  80. 176 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/TokenCleanup.cs
  81. 9 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/appsettings.Development.json
  82. 8 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/appsettings.json
  83. 37 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/css/site.css
  84. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/css/site.min.css
  85. BIN
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/favicon.ico
  86. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner1.svg
  87. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner2.svg
  88. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner3.svg
  89. 4 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/js/site.js
  90. 0 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/js/site.min.js
  91. 45 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/.bower.json
  92. 21 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/LICENSE
  93. 587 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css
  94. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map
  95. 6 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css
  96. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map
  97. 6757 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.css
  98. 1 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
  99. 6 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
  100. 0 0
      src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map

+ 15 - 1
HaBook.IES.sln

@@ -21,7 +21,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaBook.IES.IdentityServer.D
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaBook.IES.IdentityServer.EFCore.Manager", "src\component\HaBook.IES.IdentityServer.EFCore.Manager\HaBook.IES.IdentityServer.EFCore.Manager.csproj", "{3E630CB8-D1E9-42A0-BDD6-CA7101111717}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HaBook.IES.Authorization.Permission", "src\component\HaBook.IES.Authorization.Permission\HaBook.IES.Authorization.Permission.csproj", "{91821345-32A7-482A-B3AA-CCA2BA0D3543}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaBook.IES.Authorization.Permission", "src\component\HaBook.IES.Authorization.Permission\HaBook.IES.Authorization.Permission.csproj", "{91821345-32A7-482A-B3AA-CCA2BA0D3543}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaBook.IES.AzureCloud.TableStorage", "src\component\HaBook.IES.AzureCloud.TableStorage\HaBook.IES.AzureCloud.TableStorage.csproj", "{526E4661-26A0-4429-BF46-8D8D55F74F73}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaBook.IES.IdentityServer.AzureTableStorage", "src\framework\HaBook.IES.IdentityServer.AzureTableStorage\HaBook.IES.IdentityServer.AzureTableStorage.csproj", "{FE3ED1F9-377C-408B-90B2-31AB1D40C176}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -45,6 +49,14 @@ Global
 		{91821345-32A7-482A-B3AA-CCA2BA0D3543}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{91821345-32A7-482A-B3AA-CCA2BA0D3543}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{91821345-32A7-482A-B3AA-CCA2BA0D3543}.Release|Any CPU.Build.0 = Release|Any CPU
+		{526E4661-26A0-4429-BF46-8D8D55F74F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{526E4661-26A0-4429-BF46-8D8D55F74F73}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{526E4661-26A0-4429-BF46-8D8D55F74F73}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{526E4661-26A0-4429-BF46-8D8D55F74F73}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FE3ED1F9-377C-408B-90B2-31AB1D40C176}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FE3ED1F9-377C-408B-90B2-31AB1D40C176}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FE3ED1F9-377C-408B-90B2-31AB1D40C176}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FE3ED1F9-377C-408B-90B2-31AB1D40C176}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -59,6 +71,8 @@ Global
 		{AE448A55-DBE1-4490-B5CE-DFC4CC57BC19} = {9D633BF3-65C9-4459-A614-8F02E37A0F66}
 		{3E630CB8-D1E9-42A0-BDD6-CA7101111717} = {218B202D-9812-41FB-A3BC-17A622DEE174}
 		{91821345-32A7-482A-B3AA-CCA2BA0D3543} = {218B202D-9812-41FB-A3BC-17A622DEE174}
+		{526E4661-26A0-4429-BF46-8D8D55F74F73} = {218B202D-9812-41FB-A3BC-17A622DEE174}
+		{FE3ED1F9-377C-408B-90B2-31AB1D40C176} = {9D633BF3-65C9-4459-A614-8F02E37A0F66}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {2159FE62-A916-4757-8751-BD019D434CA3}

+ 17 - 0
src/component/HaBook.IES.AzureCloud.TableStorage/HaBook.IES.AzureCloud.TableStorage.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="WindowsAzure.Storage" Version="9.3.2" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Extensions.Configuration.Abstractions">
+      <HintPath>C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.configuration.abstractions\2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

+ 212 - 0
src/component/HaBook.IES.AzureCloud.TableStorage/Repositorys/AzureTableDBRepository.cs

@@ -0,0 +1,212 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+
+namespace HaBook.IES.AzureCloud.TableStorage.Repositorys
+{
+    public class AzureTableDBRepository : IAzureTableDBRepository
+
+    {
+        public IConfiguration Configuration { get; }
+        private static CloudTableClient tableClient;
+        private CloudTable _table;
+        public AzureTableDBRepository(IConfiguration configuration)
+        {
+
+            Configuration = configuration;
+            // _tableName = tableName;
+            InitializeTable();
+        }
+        private async void InitializeTable()
+        {
+            string StorageConnectionString = "DefaultEndpointsProtocol=https;AccountName=teammodelstorage;AccountKey=Yq7D4dE6cFuer2d2UZIccTA/i0c3sJ/6ITc8tNOyW+K5f+/lWw9GCos3Mxhj47PyWQgDL8YbVD63B9XcGtrMxQ==;EndpointSuffix=core.chinacloudapi.cn";
+            var storageAccount =  CloudStorageAccount.Parse(StorageConnectionString);
+            tableClient = storageAccount.CreateCloudTableClient();
+        }
+        public T DeleteById<T>(object id) where T : new()
+        {
+            throw new NotImplementedException();
+        }
+
+        public List<T> FindAll<T>() where T : TableEntity, new()
+        {
+            Type t = typeof(T);
+            CloudTable table = tableClient.GetTableReference(t.Name);
+            var exQuery =  new TableQuery<T>();
+            TableContinuationToken continuationToken = null;
+            return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.ToList<T>();
+        }
+
+        public T FindById<T>(string  id) where T : TableEntity, new()
+        {
+            Type t = typeof(T);
+            CloudTable table = tableClient.GetTableReference(t.Name);
+            var exQuery = new TableQuery<T>();
+            if (string.IsNullOrEmpty(id) )
+            {
+                exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal, id));
+            }
+            //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+            // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+            TableContinuationToken continuationToken = null;
+            return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.Single<T>();
+        }
+
+        public List<T> FindListByDict<T>(Dictionary<string, object> dict) where T  : TableEntity, new()
+        {
+            Type t = typeof(T);
+            CloudTable table = tableClient.GetTableReference(t.Name);
+
+            // table = tableClient.GetTableReference(entity.GetType().Name.ToLower());
+            var exQuery = new TableQuery<T>();
+            if (null != dict && dict.Count > 0)
+            {
+                var keys = dict.Keys;
+                foreach (String key in keys)
+                {
+                    if (dict[key] != null && !string.IsNullOrEmpty(dict[key].ToString()))
+                    {
+                        exQuery.Where(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal,
+                                                                           dict[key].ToString()));
+                    }
+                }
+            }
+            //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+            // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+            TableContinuationToken continuationToken = null;
+            return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.ToList<T>();
+        }
+
+        public List<T> FindListByDictAndLike<T>(Dictionary<string, object> dict, Dictionary<string, object> likeDict) where T : TableEntity, new()
+        {
+            throw new NotImplementedException();
+        }
+
+        public List<T> FindListByDictAndLikeAndNotEQ<T>(Dictionary<string, object> dict, Dictionary<string, object> likeDict, Dictionary<string, object> notEQDict) where T : TableEntity, new()
+        {
+            throw new NotImplementedException();
+        }
+
+        public List<T> FindListByDictAndLikeAndStartWith<T>(Dictionary<string, object> dict, Dictionary<string, object> likeDict, Dictionary<string, object> startDict) where T : TableEntity, new()
+        {
+            throw new NotImplementedException();
+        }
+
+        public List<T> FindListByKey<T>(string key, string value) where T : TableEntity, new()
+        {
+            Type t = typeof(T);
+            CloudTable table = tableClient.GetTableReference(t.Name);
+            var exQuery = new TableQuery<T>();
+            if (string.IsNullOrEmpty(key) && string.IsNullOrEmpty(value))
+            {
+                exQuery.Where(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal,  value));
+            }
+            //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+            // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+            TableContinuationToken continuationToken = null;
+            return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.ToList<T>();
+        }
+
+        public T FindOneByDict<T>(IDictionary<string, object> dict) where T : TableEntity, new()
+        {
+          
+                Type t = typeof(T);
+                CloudTable table = tableClient.GetTableReference(t.Name);
+
+                // table = tableClient.GetTableReference(entity.GetType().Name.ToLower());
+                var exQuery = new TableQuery<T>();
+                if (null != dict && dict.Count > 0)
+                {
+                    var keys = dict.Keys;
+                    foreach (String key in keys)
+                    {
+                        if (dict[key] != null && !string.IsNullOrEmpty(dict[key].ToString()))
+                        {
+                            exQuery.Where(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal,dict[key].ToString()));
+                        }
+                    }
+                }
+            //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+            // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+            exQuery.Where(TableQuery.CombineFilters(TableQuery.GenerateFilterCondition("", QueryComparisons.Equal, ""),"",""));
+                TableContinuationToken continuationToken = null;
+                return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.Single<T>();
+            }
+
+        public T FindOneByKey<T>(string key, string value) where T : TableEntity , new()
+        {
+            Type t = typeof(T);
+            CloudTable table = tableClient.GetTableReference(t.Name);
+            var exQuery = new TableQuery<T>();
+            if (string.IsNullOrEmpty(key) && string.IsNullOrEmpty(value))
+            {
+                exQuery.Where(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal,
+                                                                          value));
+            }
+            //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+            // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+            TableContinuationToken continuationToken = null;
+            return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.Single<T>();
+        }
+
+        public List<T> GetEntities<T>(IDictionary<string, object> dict) where T : TableEntity, new()
+        {
+            try
+            {
+                Type t = typeof(T);
+                CloudTable table = tableClient.GetTableReference(t.Name);
+
+                // table = tableClient.GetTableReference(entity.GetType().Name.ToLower());
+                var exQuery = new TableQuery<T>();
+                if (null != dict && dict.Count > 0)
+                {
+                    var keys = dict.Keys;
+                    foreach (String key in keys)
+                    {
+                        if (dict[key] != null && !string.IsNullOrEmpty(dict[key].ToString()))
+                        {
+                            exQuery.Where(TableQuery.GenerateFilterCondition(key, QueryComparisons.Equal,
+                                                                               dict[key].ToString()));
+                        }
+                    }
+                }
+                //exQuery. Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal,   partitionKey));
+               // exQuery.Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal,    "3e89fded-806f-454a-85fc-7f0360f5a8361"));
+                TableContinuationToken continuationToken = null;
+                return table.ExecuteQuerySegmentedAsync(exQuery, continuationToken).Result.ToList();
+
+            }
+            catch (StorageException ex)
+            {
+                Trace.TraceInformation("Unable to retrieve entity based on query specs");
+                return null;
+            }
+        }
+
+        public T Save<T>(TableEntity entity) where T : new()
+        {
+            Type t = typeof(T);
+            string name = t.Name;
+            CloudTable table = tableClient.GetTableReference(name);
+            table.CreateIfNotExistsAsync();
+            TableOperation operation = TableOperation.Insert(entity);
+            return (T)table.ExecuteAsync(operation).Result.Result;
+        }
+
+        public T Update<T>(TableEntity entity) where T : new()
+        {
+            Type t = typeof(T);
+            string name = t.Name;
+            CloudTable table = tableClient.GetTableReference(name);
+            table.CreateIfNotExistsAsync();
+            TableOperation operation = TableOperation.Replace(entity);
+            return (T)table.ExecuteAsync(operation).Result.Result;
+            throw new NotImplementedException();
+        }
+    }
+}

+ 34 - 0
src/component/HaBook.IES.AzureCloud.TableStorage/Repositorys/IAzureTableDBRepository.cs

@@ -0,0 +1,34 @@
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace HaBook.IES.AzureCloud.TableStorage.Repositorys
+{
+   public interface IAzureTableDBRepository
+    {
+        List<T> GetEntities<T>(IDictionary<string ,object> dict  ) where T : TableEntity, new();
+        T FindOneByDict<T>(IDictionary<string, object> dict) where T : TableEntity, new();
+        T FindById<T>(string  id ) where T : TableEntity, new();
+        T Save<T>(TableEntity entity) where T : new();
+        T Update<T>(TableEntity entity) where T : new();
+        T DeleteById<T>(object id) where T : new();
+        T FindOneByKey<T>(string key, string value) where T :TableEntity , new();
+        List<T> FindListByDict<T>(Dictionary<string, object> dict) where T : TableEntity, new();
+        List<T> FindListByKey<T>(string key, string value) where T : TableEntity, new();
+        List<T> FindAll<T>() where T : TableEntity, new();
+        List<T> FindListByDictAndLike<T>(
+            Dictionary<string, object> dict,
+            Dictionary<string, object> likeDict) where T : TableEntity, new();
+        List<T> FindListByDictAndLikeAndStartWith<T>(
+            Dictionary<String, Object> dict,
+            Dictionary<String, Object> likeDict,
+            Dictionary<String, Object> startDict)
+            where T : TableEntity, new();
+        List<T> FindListByDictAndLikeAndNotEQ<T>(
+            Dictionary<String, Object> dict,
+            Dictionary<String, Object> likeDict,
+            Dictionary<String, Object> notEQDict)
+          where T : TableEntity, new();
+    }
+}

File diff suppressed because it is too large
+ 276 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/Clients.cs


+ 68 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/Resources.cs

@@ -0,0 +1,68 @@
+
+using IdentityModel;
+using IdentityServer4.Models;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Configuration
+{
+    public class Resources
+    {
+        public static IEnumerable<IdentityResource> GetIdentityResources()
+        {
+            return new[]
+            {
+                // some standard scopes from the OIDC spec
+                new IdentityResources.OpenId(),
+                new IdentityResources.Profile(),
+                new IdentityResources.Email(),
+
+                // custom identity resource with some consolidated claims
+                new IdentityResource("custom.profile", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location" })
+            };
+        }
+
+        public static IEnumerable<ApiResource> GetApiResources()
+        {
+            return new[]
+            {
+                // simple version with ctor
+                new IdentityServer4.Models.ApiResource("api1", "Some API 1")
+                {
+                    // this is needed for introspection when using reference tokens
+                    ApiSecrets = { new Secret("secret".Sha256()) }
+                },
+                
+                // expanded version if more control is needed
+                new IdentityServer4.Models.ApiResource
+                {
+                    Name = "api2",
+
+                    ApiSecrets =
+                    {
+                        new IdentityServer4.Models.Secret("secret".Sha256())
+                    },
+
+                    UserClaims =
+                    {
+                        JwtClaimTypes.Name,
+                        JwtClaimTypes.Email
+                    },
+
+                    Scopes =
+                    {
+                        new Scope()
+                        {
+                            Name = "api2.full_access",
+                            DisplayName = "Full access to API 2",
+                        },
+                        new Scope
+                        {
+                            Name = "api2.read_only",
+                            DisplayName = "Read only access to API 2"
+                        }
+                    }
+                }
+            };
+        }
+    }
+}

+ 120 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/ServiceCollectionExtensions.cs

@@ -0,0 +1,120 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using HaBook.IES.IdentityServer.AzureTableStorage.DbContexts;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using System;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Storage
+{
+    /// <summary>
+    /// Extension methods to add EF database support to IdentityServer.
+    /// </summary>
+    public static class IdentityServerEntityFrameworkBuilderExtensions
+    {
+        /// <summary>
+        /// Add Configuration DbContext to the DI system.
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddConfigurationDbContext(this IServiceCollection services,
+            Action<ConfigurationStoreOptions> storeOptionsAction = null)
+        {
+            return services.AddConfigurationDbContext<ConfigurationDbContext>(storeOptionsAction);
+        }
+
+        /// <summary>
+        /// Add Configuration DbContext to the DI system.
+        /// </summary>
+        /// <typeparam name="TContext">The IConfigurationDbContext to use.</typeparam>
+        /// <param name="services"></param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddConfigurationDbContext<TContext>(this IServiceCollection services,
+        Action<ConfigurationStoreOptions> storeOptionsAction = null)
+        where TContext : DbContext, IConfigurationDbContext
+        {
+            var options = new ConfigurationStoreOptions();
+            services.AddSingleton(options);
+            storeOptionsAction?.Invoke(options);
+
+            if (options.ResolveDbContextOptions != null)
+            {
+                services.AddDbContext<TContext>(options.ResolveDbContextOptions);
+            }
+            else
+            {
+                services.AddDbContext<TContext>(dbCtxBuilder =>
+                {
+                    options.ConfigureDbContext?.Invoke(dbCtxBuilder);
+                });
+            }
+            services.AddScoped<IConfigurationDbContext, TContext>();
+
+            return services;
+        }
+
+        /// <summary>
+        /// Adds operational DbContext to the DI system.
+        /// </summary>
+        /// <param name="services"></param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddOperationalDbContext(this IServiceCollection services,
+            Action<OperationalStoreOptions> storeOptionsAction = null)
+        {
+            return services.AddOperationalDbContext<PersistedGrantDbContext>(storeOptionsAction);
+        }
+
+        /// <summary>
+        /// Adds operational DbContext to the DI system.
+        /// </summary>
+        /// <typeparam name="TContext">The IPersistedGrantDbContext to use.</typeparam>
+        /// <param name="services"></param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddOperationalDbContext<TContext>(this IServiceCollection services,
+            Action<OperationalStoreOptions> storeOptionsAction = null)
+            where TContext : DbContext, IPersistedGrantDbContext
+        {
+            var storeOptions = new OperationalStoreOptions();
+            services.AddSingleton(storeOptions);
+            storeOptionsAction?.Invoke(storeOptions);
+
+            if (storeOptions.ResolveDbContextOptions != null)
+            {
+                services.AddDbContext<TContext>(storeOptions.ResolveDbContextOptions);
+            }
+            else
+            {
+                services.AddDbContext<TContext>(dbCtxBuilder =>
+                {
+                    storeOptions.ConfigureDbContext?.Invoke(dbCtxBuilder);
+                });
+            }
+
+            services.AddScoped<IPersistedGrantDbContext, TContext>();
+            services.AddSingleton<TokenCleanup>();
+
+            return services;
+        }
+
+        /// <summary>
+        /// Adds an implementation of the IOperationalStoreNotification to the DI system.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        public static IServiceCollection AddOperationalStoreNotification<T>(this IServiceCollection services)
+           where T : class, IOperationalStoreNotification
+        {
+            services.AddTransient<IOperationalStoreNotification, T>();
+            return services;
+        }
+    }
+}

+ 40 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Configuration/TestUsers.cs

@@ -0,0 +1,40 @@
+using IdentityModel;
+using IdentityServer4;
+using IdentityServer4.Test;
+using System.Collections.Generic;
+using System.Security.Claims;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Configuration
+{
+    public class TestUsers
+    {
+        public static List<TestUser> Users = new List<TestUser>
+        {
+            new TestUser{SubjectId = "818727", Username = "alice", Password = "alice",
+                Claims =
+                {
+                    new Claim(JwtClaimTypes.Name, "Alice Smith"),
+                    new Claim(JwtClaimTypes.GivenName, "Alice"),
+                    new Claim(JwtClaimTypes.FamilyName, "Smith"),
+                    new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
+                    new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
+                    new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
+                    new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServerConstants.ClaimValueTypes.Json)
+                }
+            },
+            new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob",
+                Claims =
+                {
+                    new Claim(JwtClaimTypes.Name, "Bob Smith"),
+                    new Claim(JwtClaimTypes.GivenName, "Bob"),
+                    new Claim(JwtClaimTypes.FamilyName, "Smith"),
+                    new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
+                    new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
+                    new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
+                    new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServerConstants.ClaimValueTypes.Json),
+                    new Claim("location", "somewhere"),
+                }
+            },
+        };
+    }
+}

+ 107 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/DbContexts/ConfigurationDbContext.cs

@@ -0,0 +1,107 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using HaBook.IES.IdentityServer.AzureTableStorage.Extensions;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.DbContexts
+{
+    /// <summary>
+    /// DbContext for the IdentityServer configuration data.
+    /// </summary>
+    /// <seealso cref="Microsoft.EntityFrameworkCore.DbContext" />
+    /// <seealso cref="HaBook.IES.IdentityServer.AzureTableStorage.Interfaces.IConfigurationDbContext" />
+    public class ConfigurationDbContext : ConfigurationDbContext<ConfigurationDbContext>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationDbContext"/> class.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="storeOptions">The store options.</param>
+        /// <exception cref="ArgumentNullException">storeOptions</exception>
+        public ConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options, ConfigurationStoreOptions storeOptions)
+            : base(options, storeOptions)
+        {
+        }
+    }
+
+    /// <summary>
+    /// DbContext for the IdentityServer configuration data.
+    /// </summary>
+    /// <seealso cref="Microsoft.EntityFrameworkCore.DbContext" />
+    /// <seealso cref="HaBook.IES.IdentityServer.AzureTableStorage.Interfaces.IConfigurationDbContext" />
+    public class ConfigurationDbContext<TContext> : DbContext, IConfigurationDbContext
+        where TContext : DbContext, IConfigurationDbContext
+    {
+        private readonly ConfigurationStoreOptions storeOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ConfigurationDbContext"/> class.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="storeOptions">The store options.</param>
+        /// <exception cref="ArgumentNullException">storeOptions</exception>
+        public ConfigurationDbContext(DbContextOptions<TContext> options, ConfigurationStoreOptions storeOptions)
+            : base(options)
+        {
+            this.storeOptions = storeOptions ?? throw new ArgumentNullException(nameof(storeOptions));
+        }
+
+        /// <summary>
+        /// Gets or sets the clients.
+        /// </summary>
+        /// <value>
+        /// The clients.
+        /// </value>
+        public DbSet<Client> Clients { get; set; }
+        /// <summary>
+        /// Gets or sets the identity resources.
+        /// </summary>
+        /// <value>
+        /// The identity resources.
+        /// </value>
+        public DbSet<IdentityResource> IdentityResources { get; set; }
+        /// <summary>
+        /// Gets or sets the API resources.
+        /// </summary>
+        /// <value>
+        /// The API resources.
+        /// </value>
+        public DbSet<ApiResource> ApiResources { get; set; }
+
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        public Task<int> SaveChangesAsync()
+        {
+            return base.SaveChangesAsync();
+        }
+
+        /// <summary>
+        /// Override this method to further configure the model that was discovered by convention from the entity types
+        /// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached
+        /// and re-used for subsequent instances of your derived context.
+        /// </summary>
+        /// <param name="modelBuilder">The builder being used to construct the model for this context. Databases (and other extensions) typically
+        /// define extension methods on this object that allow you to configure aspects of the model that are specific
+        /// to a given database.</param>
+        /// <remarks>
+        /// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />)
+        /// then this method will not be run.
+        /// </remarks>
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            modelBuilder.ConfigureClientContext(storeOptions);
+            modelBuilder.ConfigureResourcesContext(storeOptions);
+
+            base.OnModelCreating(modelBuilder);
+        }
+    }
+}

+ 101 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/DbContexts/PersistedGrantDbContext.cs

@@ -0,0 +1,101 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using HaBook.IES.IdentityServer.AzureTableStorage.Extensions;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.DbContexts
+{
+    /// <summary>
+    /// DbContext for the IdentityServer operational data.
+    /// </summary>
+    /// <seealso cref="Microsoft.EntityFrameworkCore.DbContext" />
+    /// <seealso cref="HaBook.IES.IdentityServer.AzureTableStorage.Interfaces.IPersistedGrantDbContext" />
+    public class PersistedGrantDbContext : PersistedGrantDbContext<PersistedGrantDbContext>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PersistedGrantDbContext"/> class.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="storeOptions">The store options.</param>
+        /// <exception cref="ArgumentNullException">storeOptions</exception>
+        public PersistedGrantDbContext(DbContextOptions<PersistedGrantDbContext> options, OperationalStoreOptions storeOptions)
+            : base(options, storeOptions)
+        {
+        }
+    }
+
+    /// <summary>
+    /// DbContext for the IdentityServer operational data.
+    /// </summary>
+    /// <seealso cref="Microsoft.EntityFrameworkCore.DbContext" />
+    /// <seealso cref="HaBook.IES.IdentityServer.AzureTableStorage.Interfaces.IPersistedGrantDbContext" />
+    public class PersistedGrantDbContext<TContext> : DbContext, IPersistedGrantDbContext
+        where TContext : DbContext, IPersistedGrantDbContext
+    {
+        private readonly OperationalStoreOptions storeOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PersistedGrantDbContext"/> class.
+        /// </summary>
+        /// <param name="options">The options.</param>
+        /// <param name="storeOptions">The store options.</param>
+        /// <exception cref="ArgumentNullException">storeOptions</exception>
+        public PersistedGrantDbContext(DbContextOptions options, OperationalStoreOptions storeOptions)
+            : base(options)
+        {
+            if (storeOptions == null) throw new ArgumentNullException(nameof(storeOptions));
+            this.storeOptions = storeOptions;
+        }
+
+        /// <summary>
+        /// Gets or sets the persisted grants.
+        /// </summary>
+        /// <value>
+        /// The persisted grants.
+        /// </value>
+        public DbSet<PersistedGrant> PersistedGrants { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device codes.
+        /// </summary>
+        /// <value>
+        /// The device codes.
+        /// </value>
+        public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
+
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        public Task<int> SaveChangesAsync()
+        {
+            return base.SaveChangesAsync();
+        }
+
+        /// <summary>
+        /// Override this method to further configure the model that was discovered by convention from the entity types
+        /// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached
+        /// and re-used for subsequent instances of your derived context.
+        /// </summary>
+        /// <param name="modelBuilder">The builder being used to construct the model for this context. Databases (and other extensions) typically
+        /// define extension methods on this object that allow you to configure aspects of the model that are specific
+        /// to a given database.</param>
+        /// <remarks>
+        /// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />)
+        /// then this method will not be run.
+        /// </remarks>
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            modelBuilder.ConfigurePersistedGrantContext(storeOptions);
+
+            base.OnModelCreating(modelBuilder);
+        }
+    }
+}

+ 28 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResource.cs

@@ -0,0 +1,28 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+#pragma warning disable 1591
+
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiResource : TableEntity
+    {
+        public int Id { get; set; }
+        public bool Enabled { get; set; } = true;
+        public string Name { get; set; }
+        public string DisplayName { get; set; }
+        public string Description { get; set; }
+        public List<ApiSecret> Secrets { get; set; }
+        public List<ApiScope> Scopes { get; set; }
+        public List<ApiResourceClaim> UserClaims { get; set; }
+        public List<ApiResourceProperty> Properties { get; set; }
+        public DateTime Created { get; set; } = DateTime.UtcNow;
+        public DateTime? Updated { get; set; }
+        public DateTime? LastAccessed { get; set; }
+        public bool NonEditable { get; set; }
+    }
+}

+ 13 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResourceClaim.cs

@@ -0,0 +1,13 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiResourceClaim : UserClaim
+    {
+        public int ApiResourceId { get; set; }
+        public ApiResource ApiResource { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiResourceProperty.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiResourceProperty : Property
+    {
+        public int ApiResourceId { get; set; }
+        public ApiResource ApiResource { get; set; }
+    }
+}

+ 25 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiScope.cs

@@ -0,0 +1,25 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+#pragma warning disable 1591
+
+using Microsoft.WindowsAzure.Storage.Table;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiScope :TableEntity
+    {
+        public int Id { get; set; }
+        public string Name { get; set; }
+        public string DisplayName { get; set; }
+        public string Description { get; set; }
+        public bool Required { get; set; }
+        public bool Emphasize { get; set; }
+        public bool ShowInDiscoveryDocument { get; set; } = true;
+        public List<ApiScopeClaim> UserClaims { get; set; }
+
+        public int ApiResourceId { get; set; }
+        public ApiResource ApiResource { get; set; }
+    }
+}

+ 13 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiScopeClaim.cs

@@ -0,0 +1,13 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiScopeClaim : UserClaim
+    {
+        public int ApiScopeId { get; set; }
+        public ApiScope ApiScope { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ApiSecret.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ApiSecret : Secret
+    {
+        public int ApiResourceId { get; set; }
+        public ApiResource ApiResource { get; set; }
+    }
+}

+ 68 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Client.cs

@@ -0,0 +1,68 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+using IdentityServer4.Models;
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class Client :TableEntity
+    {
+        public int Id { get; set; }
+        public bool Enabled { get; set; } = true;
+        public string ClientId { get; set; }
+        public string ProtocolType { get; set; } = "oidc";
+        public List<ClientSecret> ClientSecrets { get; set; }
+        public bool RequireClientSecret { get; set; } = true;
+        public string ClientName { get; set; }
+        public string Description { get; set; }
+        public string ClientUri { get; set; }
+        public string LogoUri { get; set; }
+        public bool RequireConsent { get; set; } = true;
+        public bool AllowRememberConsent { get; set; } = true;
+        public bool AlwaysIncludeUserClaimsInIdToken { get; set; }
+        public List<ClientGrantType> AllowedGrantTypes { get; set; }
+        public bool RequirePkce { get; set; }
+        public bool AllowPlainTextPkce { get; set; }
+        public bool AllowAccessTokensViaBrowser { get; set; }
+        public List<ClientRedirectUri> RedirectUris { get; set; }
+        public List<ClientPostLogoutRedirectUri> PostLogoutRedirectUris { get; set; }
+        public string FrontChannelLogoutUri { get; set; }
+        public bool FrontChannelLogoutSessionRequired { get; set; } = true;
+        public string BackChannelLogoutUri { get; set; }
+        public bool BackChannelLogoutSessionRequired { get; set; } = true;
+        public bool AllowOfflineAccess { get; set; }
+        public List<ClientScope> AllowedScopes { get; set; }
+        public int IdentityTokenLifetime { get; set; } = 300;
+        public int AccessTokenLifetime { get; set; } = 3600;
+        public int AuthorizationCodeLifetime { get; set; } = 300;
+        public int? ConsentLifetime { get; set; } = null;
+        public int AbsoluteRefreshTokenLifetime { get; set; } = 2592000;
+        public int SlidingRefreshTokenLifetime { get; set; } = 1296000;
+        public int RefreshTokenUsage { get; set; } = (int)TokenUsage.OneTimeOnly;
+        public bool UpdateAccessTokenClaimsOnRefresh { get; set; }
+        public int RefreshTokenExpiration { get; set; } = (int)TokenExpiration.Absolute;
+        public int AccessTokenType { get; set; } = (int)0; // AccessTokenType.Jwt;
+        public bool EnableLocalLogin { get; set; } = true;
+        public List<ClientIdPRestriction> IdentityProviderRestrictions { get; set; }
+        public bool IncludeJwtId { get; set; }
+        public List<ClientClaim> Claims { get; set; }
+        public bool AlwaysSendClientClaims { get; set; }
+        public string ClientClaimsPrefix { get; set; } = "client_";
+        public string PairWiseSubjectSalt { get; set; }
+        public List<ClientCorsOrigin> AllowedCorsOrigins { get; set; }
+        public List<ClientProperty> Properties { get; set; }
+        public DateTime Created { get; set; } = DateTime.UtcNow;
+        public DateTime? Updated { get; set; }
+        public DateTime? LastAccessed { get; set; }
+        public int? UserSsoLifetime { get; set; }
+        public string UserCodeType { get; set; }
+        public int DeviceCodeLifetime { get; set; } = 300;
+        public bool NonEditable { get; set; }
+    }
+}

+ 18 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientClaim.cs

@@ -0,0 +1,18 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientClaim
+    {
+        public int Id { get; set; }
+        public string Type { get; set; }
+        public string Value { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 19 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientCorsOrigin.cs

@@ -0,0 +1,19 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+using Microsoft.WindowsAzure.Storage.Table;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientCorsOrigin : TableEntity
+    {
+        public int Id { get; set; }
+        public string Origin { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientGrantType.cs

@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientGrantType
+    {
+        public int Id { get; set; }
+        public string GrantType { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientIdPRestriction.cs

@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientIdPRestriction
+    {
+        public int Id { get; set; }
+        public string Provider { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientPostLogoutRedirectUri.cs

@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientPostLogoutRedirectUri
+    {
+        public int Id { get; set; }
+        public string PostLogoutRedirectUri { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientProperty.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientProperty : Property
+    {
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientRedirectUri.cs

@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientRedirectUri
+    {
+        public int Id { get; set; }
+        public string RedirectUri { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientScope.cs

@@ -0,0 +1,17 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientScope
+    {
+        public int Id { get; set; }
+        public string Scope { get; set; }
+
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/ClientSecret.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class ClientSecret : Secret
+    {
+        public int ClientId { get; set; }
+        public Client Client { get; set; }
+    }
+}

+ 16 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/DeviceFlowCodes.cs

@@ -0,0 +1,16 @@
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class DeviceFlowCodes :TableEntity
+    {
+        public string DeviceCode { get; set; }
+        public string UserCode { get; set; }
+        public string SubjectId { get; set; }
+        public string ClientId { get; set; }
+        public DateTime CreationTime { get; set; }
+        public DateTime? Expiration { get; set; }
+        public string Data { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityClaim.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class IdentityClaim : UserClaim
+    {
+        public int IdentityResourceId { get; set; }
+        public IdentityResource IdentityResource { get; set; }
+    }
+}

+ 29 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityResource.cs

@@ -0,0 +1,29 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class IdentityResource :TableEntity
+    {
+        public int Id { get; set; }
+        public bool Enabled { get; set; } = true;
+        public string Name { get; set; }
+        public string DisplayName { get; set; }
+        public string Description { get; set; }
+        public bool Required { get; set; }
+        public bool Emphasize { get; set; }
+        public bool ShowInDiscoveryDocument { get; set; } = true;
+        public List<IdentityClaim> UserClaims { get; set; }
+        public List<IdentityResourceProperty> Properties { get; set; }
+        public DateTime Created { get; set; } = DateTime.UtcNow;
+        public DateTime? Updated { get; set; }
+        public bool NonEditable { get; set; }
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/IdentityResourceProperty.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class IdentityResourceProperty : Property
+    {
+        public int IdentityResourceId { get; set; }
+        public IdentityResource IdentityResource { get; set; }
+    }
+}

+ 22 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/PersistedGrant.cs

@@ -0,0 +1,22 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public class PersistedGrant : TableEntity
+    {
+        public string Key { get; set; }
+        public string Type { get; set; }
+        public string SubjectId { get; set; }
+        public string ClientId { get; set; }
+        public DateTime CreationTime { get; set; }
+        public DateTime? Expiration { get; set; }
+        public string Data { get; set; }
+    }
+}

+ 15 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Property.cs

@@ -0,0 +1,15 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public abstract class Property
+    {
+        public int Id { get; set; }
+        public string Key { get; set; }
+        public string Value { get; set; }
+    }
+}

+ 20 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/Secret.cs

@@ -0,0 +1,20 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+using System;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public abstract class Secret
+    {
+        public int Id { get; set; }
+        public string Description { get; set; }
+        public string Value { get; set; }
+        public DateTime? Expiration { get; set; }
+        public string Type { get; set; } = "SharedSecret";
+        public DateTime Created { get; set; } = DateTime.UtcNow;
+    }
+}

+ 14 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Entities/UserClaim.cs

@@ -0,0 +1,14 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+#pragma warning disable 1591
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Entities
+{
+    public abstract class UserClaim
+    {
+        public int Id { get; set; }
+        public string Type { get; set; }
+    }
+}

+ 113 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/IdentityServerEntityFrameworkBuilderExtensions.cs

@@ -0,0 +1,113 @@
+
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using HaBook.IES.IdentityServer.AzureTableStorage.Services;
+using HaBook.IES.IdentityServer.AzureTableStorage.Storage;
+using HaBook.IES.IdentityServer.AzureTableStorage.Stores;
+using IdentityServer4.Stores;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System;
+using PersistedGrantStore = HaBook.IES.IdentityServer.AzureTableStorage.Options.PersistedGrantStore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Extensions
+{
+    /// <summary>
+    /// Extension methods to add EF database support to IdentityServer.
+    /// </summary>
+    public static class IdentityServerEntityFrameworkBuilderExtensions
+    {
+        /// <summary>
+        /// Configures EF implementation of IClientStore, IResourceStore, and ICorsPolicyService with IdentityServer.
+        /// </summary>
+        /// <param name="builder">The builder.</param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IIdentityServerBuilder AddConfigurationStore(
+            this IIdentityServerBuilder builder,
+             string connectionString,
+            Action<ConfigurationStoreOptions> storeOptionsAction = null)
+        {
+            builder.Services.Configure<DatabaseOptions>(dbOptions =>
+            {
+                dbOptions.ConnectionString = connectionString;
+            });
+            var storeOptions = new ConfigurationStoreOptions();
+            storeOptionsAction?.Invoke(storeOptions);
+            builder.Services.AddSingleton(storeOptions);
+
+
+            builder.AddClientStore<ClientStore>();
+            builder.AddResourceStore<ResourceStore>();
+            builder.AddCorsPolicyService<CorsPolicyService>();
+
+            return builder;
+        }
+
+        /// <summary>
+        /// Configures caching for IClientStore, IResourceStore, and ICorsPolicyService with IdentityServer.
+        /// </summary>
+        /// <param name="builder">The builder.</param>
+        /// <returns></returns>
+        public static IIdentityServerBuilder AddConfigurationStoreCache(
+            this IIdentityServerBuilder builder)
+        {
+            builder.AddInMemoryCaching();
+
+            // add the caching decorators
+            builder.AddClientStoreCache<ClientStore>();
+            builder.AddResourceStoreCache<ResourceStore>();
+            builder.AddCorsPolicyCache<CorsPolicyService>();
+            return builder;
+        }
+
+        /// <summary>
+        /// Configures EF implementation of IPersistedGrantStore with IdentityServer.
+        /// </summary>
+        /// <param name="builder">The builder.</param>
+        /// <param name="storeOptionsAction">The store options action.</param>
+        /// <returns></returns>
+        public static IIdentityServerBuilder AddOperationalStore(
+            this IIdentityServerBuilder builder,
+            string connectionString,
+            Action<OperationalStoreOptions> storeOptionsAction = null,
+            Action<TokenCleanupOptions> tokenCleanUpOptions = null)
+        {
+
+            builder.Services.Configure<DatabaseOptions>(dbOptions =>
+            {
+                dbOptions.ConnectionString = connectionString;
+            });
+
+            builder.Services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();
+            var storeOptions = new OperationalStoreOptions();
+            storeOptionsAction?.Invoke(storeOptions);
+            builder.Services.AddSingleton(storeOptions);
+
+            var tokenCleanupOptions = new TokenCleanupOptions();
+            tokenCleanUpOptions?.Invoke(tokenCleanupOptions);
+            builder.Services.AddSingleton(tokenCleanupOptions);
+            builder.Services.AddSingleton<TokenCleanup>();
+            builder.Services.AddTransient<IDeviceFlowStore, DeviceFlowStore>();
+            builder.Services.AddSingleton<IHostedService, TokenCleanupHost>();
+
+            return builder;
+
+
+         
+        }
+
+        /// <summary>
+        /// Adds an implementation of the IOperationalStoreNotification to IdentityServer.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="builder"></param>
+        /// <returns></returns>
+        public static IIdentityServerBuilder AddOperationalStoreNotification<T>(
+           this IIdentityServerBuilder builder)
+           where T : class, IOperationalStoreNotification
+        {
+            builder.Services.AddOperationalStoreNotification<T>();
+            return builder;
+        }
+    }
+}

+ 269 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/ModelBuilderExtensions.cs

@@ -0,0 +1,269 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Extensions
+{
+    /// <summary>
+    /// Extension methods to define the database schema for the configuration and operational data stores.
+    /// </summary>
+    public static class ModelBuilderExtensions
+    {
+        private static EntityTypeBuilder<TEntity> ToTable<TEntity>(this EntityTypeBuilder<TEntity> entityTypeBuilder, TableConfiguration configuration)
+            where TEntity : class
+        {
+            return string.IsNullOrWhiteSpace(configuration.Schema) ? entityTypeBuilder.ToTable(configuration.Name) : entityTypeBuilder.ToTable(configuration.Name, configuration.Schema);
+        }
+
+        /// <summary>
+        /// Configures the client context.
+        /// </summary>
+        /// <param name="modelBuilder">The model builder.</param>
+        /// <param name="storeOptions">The store options.</param>
+        public static void ConfigureClientContext(this ModelBuilder modelBuilder, ConfigurationStoreOptions storeOptions)
+        {
+            if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema)) modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
+
+            modelBuilder.Entity<Client>(client =>
+            {
+                client.ToTable(storeOptions.Client);
+                client.HasKey(x => x.Id);
+
+                client.Property(x => x.ClientId).HasMaxLength(200).IsRequired();
+                client.Property(x => x.ProtocolType).HasMaxLength(200).IsRequired();
+                client.Property(x => x.ClientName).HasMaxLength(200);
+                client.Property(x => x.ClientUri).HasMaxLength(2000);
+                client.Property(x => x.LogoUri).HasMaxLength(2000);
+                client.Property(x => x.Description).HasMaxLength(1000);
+                client.Property(x => x.FrontChannelLogoutUri).HasMaxLength(2000);
+                client.Property(x => x.BackChannelLogoutUri).HasMaxLength(2000);
+                client.Property(x => x.ClientClaimsPrefix).HasMaxLength(200);
+                client.Property(x => x.PairWiseSubjectSalt).HasMaxLength(200);
+                client.Property(x => x.UserCodeType).HasMaxLength(100);
+
+                client.HasIndex(x => x.ClientId).IsUnique();
+
+                client.HasMany(x => x.AllowedGrantTypes).WithOne(x => x.Client).HasForeignKey(x=>x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.RedirectUris).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.PostLogoutRedirectUris).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.AllowedScopes).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.ClientSecrets).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.Claims).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.IdentityProviderRestrictions).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.AllowedCorsOrigins).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                client.HasMany(x => x.Properties).WithOne(x => x.Client).HasForeignKey(x => x.ClientId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+            });
+
+            modelBuilder.Entity<ClientGrantType>(grantType =>
+            {
+                grantType.ToTable(storeOptions.ClientGrantType);
+                grantType.Property(x => x.GrantType).HasMaxLength(250).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientRedirectUri>(redirectUri =>
+            {
+                redirectUri.ToTable(storeOptions.ClientRedirectUri);
+                redirectUri.Property(x => x.RedirectUri).HasMaxLength(2000).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientPostLogoutRedirectUri>(postLogoutRedirectUri =>
+            {
+                postLogoutRedirectUri.ToTable(storeOptions.ClientPostLogoutRedirectUri);
+                postLogoutRedirectUri.Property(x => x.PostLogoutRedirectUri).HasMaxLength(2000).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientScope>(scope =>
+            {
+                scope.ToTable(storeOptions.ClientScopes);
+                scope.Property(x => x.Scope).HasMaxLength(200).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientSecret>(secret =>
+            {
+                secret.ToTable(storeOptions.ClientSecret);
+                secret.Property(x => x.Value).HasMaxLength(4000).IsRequired();
+                secret.Property(x => x.Type).HasMaxLength(250).IsRequired();
+                secret.Property(x => x.Description).HasMaxLength(2000);
+            });
+
+            modelBuilder.Entity<ClientClaim>(claim =>
+            {
+                claim.ToTable(storeOptions.ClientClaim);
+                claim.Property(x => x.Type).HasMaxLength(250).IsRequired();
+                claim.Property(x => x.Value).HasMaxLength(250).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientIdPRestriction>(idPRestriction =>
+            {
+                idPRestriction.ToTable(storeOptions.ClientIdPRestriction);
+                idPRestriction.Property(x => x.Provider).HasMaxLength(200).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientCorsOrigin>(corsOrigin =>
+            {
+                corsOrigin.ToTable(storeOptions.ClientCorsOrigin);
+                corsOrigin.Property(x => x.Origin).HasMaxLength(150).IsRequired();
+            });
+
+            modelBuilder.Entity<ClientProperty>(property =>
+            {
+                property.ToTable(storeOptions.ClientProperty);
+                property.Property(x => x.Key).HasMaxLength(250).IsRequired();
+                property.Property(x => x.Value).HasMaxLength(2000).IsRequired();
+            });
+        }
+
+        /// <summary>
+        /// Configures the persisted grant context.
+        /// </summary>
+        /// <param name="modelBuilder">The model builder.</param>
+        /// <param name="storeOptions">The store options.</param>
+        public static void ConfigurePersistedGrantContext(this ModelBuilder modelBuilder, OperationalStoreOptions storeOptions)
+        {
+            if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema)) modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
+
+            modelBuilder.Entity<PersistedGrant>(grant =>
+            {
+                grant.ToTable(storeOptions.PersistedGrants);
+
+                grant.Property(x => x.Key).HasMaxLength(200).ValueGeneratedNever();
+                grant.Property(x => x.Type).HasMaxLength(50).IsRequired();
+                grant.Property(x => x.SubjectId).HasMaxLength(200);
+                grant.Property(x => x.ClientId).HasMaxLength(200).IsRequired();
+                grant.Property(x => x.CreationTime).IsRequired();
+                // 50000 chosen to be explicit to allow enough size to avoid truncation, yet stay beneath the MySql row size limit of ~65K
+                // apparently anything over 4K converts to nvarchar(max) on SqlServer
+                grant.Property(x => x.Data).HasMaxLength(50000).IsRequired();
+
+                grant.HasKey(x => x.Key);
+
+                grant.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type });
+            });
+
+            modelBuilder.Entity<DeviceFlowCodes>(codes =>
+            {
+                codes.ToTable(storeOptions.DeviceFlowCodes);
+
+                codes.Property(x => x.DeviceCode).HasMaxLength(200).IsRequired();
+                codes.Property(x => x.UserCode).HasMaxLength(200).IsRequired();
+                codes.Property(x => x.SubjectId).HasMaxLength(200);
+                codes.Property(x => x.ClientId).HasMaxLength(200).IsRequired();
+                codes.Property(x => x.CreationTime).IsRequired();
+                codes.Property(x => x.Expiration).IsRequired();
+                // 50000 chosen to be explicit to allow enough size to avoid truncation, yet stay beneath the MySql row size limit of ~65K
+                // apparently anything over 4K converts to nvarchar(max) on SqlServer
+                codes.Property(x => x.Data).HasMaxLength(50000).IsRequired();
+
+                codes.HasKey(x => new {x.UserCode});
+
+                codes.HasIndex(x => x.DeviceCode).IsUnique();
+                codes.HasIndex(x => x.UserCode).IsUnique();
+            });
+        }
+
+        /// <summary>
+        /// Configures the resources context.
+        /// </summary>
+        /// <param name="modelBuilder">The model builder.</param>
+        /// <param name="storeOptions">The store options.</param>
+        public static void ConfigureResourcesContext(this ModelBuilder modelBuilder, ConfigurationStoreOptions storeOptions)
+        {
+            if (!string.IsNullOrWhiteSpace(storeOptions.DefaultSchema)) modelBuilder.HasDefaultSchema(storeOptions.DefaultSchema);
+
+            modelBuilder.Entity<IdentityResource>(identityResource =>
+            {
+                identityResource.ToTable(storeOptions.IdentityResource).HasKey(x => x.Id);
+
+                identityResource.Property(x => x.Name).HasMaxLength(200).IsRequired();
+                identityResource.Property(x => x.DisplayName).HasMaxLength(200);
+                identityResource.Property(x => x.Description).HasMaxLength(1000);
+
+                identityResource.HasIndex(x => x.Name).IsUnique();
+
+                identityResource.HasMany(x => x.UserClaims).WithOne(x => x.IdentityResource).HasForeignKey(x => x.IdentityResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                identityResource.HasMany(x => x.Properties).WithOne(x => x.IdentityResource).HasForeignKey(x => x.IdentityResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+            });
+
+            modelBuilder.Entity<IdentityClaim>(claim =>
+            {
+                claim.ToTable(storeOptions.IdentityClaim).HasKey(x => x.Id);
+
+                claim.Property(x => x.Type).HasMaxLength(200).IsRequired();
+            });
+
+            modelBuilder.Entity<IdentityResourceProperty>(property =>
+            {
+                property.ToTable(storeOptions.IdentityResourceProperty);
+                property.Property(x => x.Key).HasMaxLength(250).IsRequired();
+                property.Property(x => x.Value).HasMaxLength(2000).IsRequired();
+            });
+
+
+
+            modelBuilder.Entity<ApiResource>(apiResource =>
+            {
+                apiResource.ToTable(storeOptions.ApiResource).HasKey(x => x.Id);
+
+                apiResource.Property(x => x.Name).HasMaxLength(200).IsRequired();
+                apiResource.Property(x => x.DisplayName).HasMaxLength(200);
+                apiResource.Property(x => x.Description).HasMaxLength(1000);
+
+                apiResource.HasIndex(x => x.Name).IsUnique();
+
+                apiResource.HasMany(x => x.Secrets).WithOne(x => x.ApiResource).HasForeignKey(x => x.ApiResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                apiResource.HasMany(x => x.Scopes).WithOne(x => x.ApiResource).HasForeignKey(x => x.ApiResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                apiResource.HasMany(x => x.UserClaims).WithOne(x => x.ApiResource).HasForeignKey(x => x.ApiResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+                apiResource.HasMany(x => x.Properties).WithOne(x => x.ApiResource).HasForeignKey(x => x.ApiResourceId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+            });
+
+            modelBuilder.Entity<ApiSecret>(apiSecret =>
+            {
+                apiSecret.ToTable(storeOptions.ApiSecret).HasKey(x => x.Id);
+
+                apiSecret.Property(x => x.Description).HasMaxLength(1000);
+                apiSecret.Property(x => x.Value).HasMaxLength(4000).IsRequired();
+                apiSecret.Property(x => x.Type).HasMaxLength(250).IsRequired();
+            });
+
+            modelBuilder.Entity<ApiResourceClaim>(apiClaim =>
+            {
+                apiClaim.ToTable(storeOptions.ApiClaim).HasKey(x => x.Id);
+
+                apiClaim.Property(x => x.Type).HasMaxLength(200).IsRequired();
+            });
+
+            modelBuilder.Entity<ApiScope>(apiScope =>
+            {
+                apiScope.ToTable(storeOptions.ApiScope).HasKey(x => x.Id);
+
+                apiScope.Property(x => x.Name).HasMaxLength(200).IsRequired();
+                apiScope.Property(x => x.DisplayName).HasMaxLength(200);
+                apiScope.Property(x => x.Description).HasMaxLength(1000);
+
+                apiScope.HasIndex(x => x.Name).IsUnique();
+
+                apiScope.HasMany(x => x.UserClaims).WithOne(x => x.ApiScope).HasForeignKey(x => x.ApiScopeId).IsRequired().OnDelete(DeleteBehavior.Cascade);
+            });
+
+            modelBuilder.Entity<ApiScopeClaim>(apiScopeClaim =>
+            {
+                apiScopeClaim.ToTable(storeOptions.ApiScopeClaim).HasKey(x => x.Id);
+
+                apiScopeClaim.Property(x => x.Type).HasMaxLength(200).IsRequired();
+            });
+
+            modelBuilder.Entity<ApiResourceProperty>(property =>
+            {
+                property.ToTable(storeOptions.ApiResourceProperty);
+                property.Property(x => x.Key).HasMaxLength(250).IsRequired();
+                property.Property(x => x.Value).HasMaxLength(2000).IsRequired();
+            });
+
+        }
+    }
+}

+ 40 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Extensions/TokenCleanupHost.cs

@@ -0,0 +1,40 @@
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.Extensions.Hosting;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Extensions
+{
+    internal class TokenCleanupHost : IHostedService
+    {
+        private readonly TokenCleanup _tokenCleanup;
+        private readonly OperationalStoreOptions _options;
+
+        public TokenCleanupHost(TokenCleanup tokenCleanup, OperationalStoreOptions options)
+        {
+            _tokenCleanup = tokenCleanup;
+            _options = options;
+        }
+
+        public Task StartAsync(CancellationToken cancellationToken)
+        {
+            if (_options.EnableTokenCleanup)
+            {
+                _tokenCleanup.Start(cancellationToken);
+            }
+            return Task.CompletedTask;
+        }
+
+        public Task StopAsync(CancellationToken cancellationToken)
+        {
+            if (_options.EnableTokenCleanup)
+            {
+                _tokenCleanup.Stop();
+            }
+            return Task.CompletedTask;
+        }
+    }
+}

+ 16 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/HaBook.IES.IdentityServer.AzureTableStorage.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="AutoMapper" Version="7.0.1" />
+    <PackageReference Include="IdentityServer4" Version="2.3.0-preview2" />
+    <PackageReference Include="Microsoft.AspNetCore.App" />
+    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.1.1" />
+    <PackageReference Include="WindowsAzure.Storage" Version="9.3.2" />
+  </ItemGroup>
+
+</Project>

+ 54 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Interfaces/IConfigurationDbContext.cs

@@ -0,0 +1,54 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Interfaces
+{
+    /// <summary>
+    /// Abstraction for the configuration context.
+    /// </summary>
+    /// <seealso cref="System.IDisposable" />
+    public interface IConfigurationDbContext : IDisposable
+    {
+        /// <summary>
+        /// Gets or sets the clients.
+        /// </summary>
+        /// <value>
+        /// The clients.
+        /// </value>
+        DbSet<Client> Clients { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the identity resources.
+        /// </summary>
+        /// <value>
+        /// The identity resources.
+        /// </value>
+        DbSet<IdentityResource> IdentityResources { get; set; }
+        
+        /// <summary>
+        /// Gets or sets the API resources.
+        /// </summary>
+        /// <value>
+        /// The API resources.
+        /// </value>
+        DbSet<ApiResource> ApiResources { get; set; }
+
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        int SaveChanges();
+        
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        Task<int> SaveChangesAsync();
+    }
+}

+ 46 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Interfaces/IPersistedGrantDbContext.cs

@@ -0,0 +1,46 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Interfaces
+{
+    /// <summary>
+    /// Abstraction for the operational data context.
+    /// </summary>
+    /// <seealso cref="System.IDisposable" />
+    public interface IPersistedGrantDbContext : IDisposable
+    {
+        /// <summary>
+        /// Gets or sets the persisted grants.
+        /// </summary>
+        /// <value>
+        /// The persisted grants.
+        /// </value>
+        DbSet<PersistedGrant> PersistedGrants { get; set; }
+
+        /// <summary>
+        /// Gets or sets the device flow codes.
+        /// </summary>
+        /// <value>
+        /// The device flow codes.
+        /// </value>
+        DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
+
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        int SaveChanges();
+
+        /// <summary>
+        /// Saves the changes.
+        /// </summary>
+        /// <returns></returns>
+        Task<int> SaveChangesAsync();
+    }
+}

+ 48 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ApiResourceMapperProfile.cs

@@ -0,0 +1,48 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+using AutoMapper;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Defines entity/model mapping for API resources.
+    /// </summary>
+    /// <seealso cref="AutoMapper.Profile" />
+    public class ApiResourceMapperProfile : Profile
+    {
+        /// <summary>
+        /// <see cref="ApiResourceMapperProfile"/>
+        /// </summary>
+        public ApiResourceMapperProfile()
+        {
+            CreateMap<Entities.ApiResourceProperty, KeyValuePair<string, string>>()
+                .ReverseMap();
+
+            CreateMap<Entities.ApiResource, Models.ApiResource>(MemberList.Destination)
+                .ConstructUsing(src => new Models.ApiResource())
+                .ForMember(x => x.ApiSecrets, opts => opts.MapFrom(x => x.Secrets))
+                .ReverseMap();
+
+            CreateMap<Entities.ApiResourceClaim, string>()
+                .ConstructUsing(x => x.Type)
+                .ReverseMap()
+                .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ApiSecret, Models.Secret>(MemberList.Destination)
+                .ForMember(dest => dest.Type, opt => opt.Condition(srs => srs != null))
+                .ReverseMap();
+
+            CreateMap<Entities.ApiScope, Models.Scope>(MemberList.Destination)
+                .ConstructUsing(src => new Models.Scope())
+                .ReverseMap();
+
+            CreateMap<Entities.ApiScopeClaim, string>()
+               .ConstructUsing(x => x.Type)
+               .ReverseMap()
+               .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src));
+        }
+    }
+}

+ 43 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ApiResourceMappers.cs

@@ -0,0 +1,43 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using AutoMapper;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Extension methods to map to/from entity/model for API resources.
+    /// </summary>
+    public static class ApiResourceMappers
+    {
+        static ApiResourceMappers()
+        {
+            Mapper = new MapperConfiguration(cfg => cfg.AddProfile<ApiResourceMapperProfile>())
+                .CreateMapper();
+        }
+
+        internal static IMapper Mapper { get; }
+
+        /// <summary>
+        /// Maps an entity to a model.
+        /// </summary>
+        /// <param name="entity">The entity.</param>
+        /// <returns></returns>
+        public static Models.ApiResource ToModel(this ApiResource entity)
+        {
+            return entity == null ? null : Mapper.Map<Models.ApiResource>(entity);
+        }
+
+        /// <summary>
+        /// Maps a model to an entity.
+        /// </summary>
+        /// <param name="model">The model.</param>
+        /// <returns></returns>
+        public static ApiResource ToEntity(this Models.ApiResource model)
+        {
+            return model == null ? null : Mapper.Map<ApiResource>(model);
+        }
+    }
+}

+ 70 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ClientMapperProfile.cs

@@ -0,0 +1,70 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+using System.Security.Claims;
+using AutoMapper;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Defines entity/model mapping for clients.
+    /// </summary>
+    /// <seealso cref="AutoMapper.Profile" />
+    public class ClientMapperProfile : Profile
+    {
+        /// <summary>
+        /// <see>
+        ///     <cref>{ClientMapperProfile}</cref>
+        /// </see>
+        /// </summary>
+        public ClientMapperProfile()
+        {
+            CreateMap<Entities.ClientProperty, KeyValuePair<string, string>>()
+                .ReverseMap();
+
+            CreateMap<Entities.Client, Models.Client>()
+                .ForMember(dest => dest.ProtocolType, opt => opt.Condition(srs => srs != null))
+                .ReverseMap();
+
+            CreateMap<Entities.ClientCorsOrigin, string>()
+                .ConstructUsing(src => src.Origin)
+                .ReverseMap()
+                .ForMember(dest => dest.Origin, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientIdPRestriction, string>()
+                .ConstructUsing(src => src.Provider)
+                .ReverseMap()
+                .ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientClaim, Claim>(MemberList.None)
+                .ConstructUsing(src => new Claim(src.Type, src.Value))
+                .ReverseMap();
+
+            CreateMap<Entities.ClientScope, string>()
+                .ConstructUsing(src => src.Scope)
+                .ReverseMap()
+                .ForMember(dest => dest.Scope, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientPostLogoutRedirectUri, string>()
+                .ConstructUsing(src => src.PostLogoutRedirectUri)
+                .ReverseMap()
+                .ForMember(dest => dest.PostLogoutRedirectUri, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientRedirectUri, string>()
+                .ConstructUsing(src => src.RedirectUri)
+                .ReverseMap()
+                .ForMember(dest => dest.RedirectUri, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientGrantType, string>()
+                .ConstructUsing(src => src.GrantType)
+                .ReverseMap()
+                .ForMember(dest => dest.GrantType, opt => opt.MapFrom(src => src));
+
+            CreateMap<Entities.ClientSecret, Models.Secret>(MemberList.Destination)
+                .ForMember(dest => dest.Type, opt => opt.Condition(srs => srs != null))
+                .ReverseMap();
+        }
+    }
+}

+ 42 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/ClientMappers.cs

@@ -0,0 +1,42 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using AutoMapper;
+using Models=IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Extension methods to map to/from entity/model for clients.
+    /// </summary>
+    public static class ClientMappers
+    {
+        static ClientMappers()
+        {
+            Mapper = new MapperConfiguration(cfg => cfg.AddProfile<ClientMapperProfile>())
+                .CreateMapper();
+        }
+
+        internal static IMapper Mapper { get; }
+
+        /// <summary>
+        /// Maps an entity to a model.
+        /// </summary>
+        /// <param name="entity">The entity.</param>
+        /// <returns></returns>
+        public static Models.Client ToModel(this Entities.Client entity)
+        {
+            return Mapper.Map<Models.Client>(entity);
+        }
+
+        /// <summary>
+        /// Maps a model to an entity.
+        /// </summary>
+        /// <param name="model">The model.</param>
+        /// <returns></returns>
+        public static Entities.Client ToEntity(this Models.Client model)
+        {
+            return Mapper.Map<Entities.Client>(model);
+        }
+    }
+}

+ 34 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/IdentityResourceMapperProfile.cs

@@ -0,0 +1,34 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+using AutoMapper;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Defines entity/model mapping for identity resources.
+    /// </summary>
+    /// <seealso cref="AutoMapper.Profile" />
+    public class IdentityResourceMapperProfile : Profile
+    {
+        /// <summary>
+        /// <see cref="IdentityResourceMapperProfile"/>
+        /// </summary>
+        public IdentityResourceMapperProfile()
+        {
+            CreateMap<Entities.IdentityResourceProperty, KeyValuePair<string, string>>()
+                .ReverseMap();
+
+            CreateMap<Entities.IdentityResource, Models.IdentityResource>(MemberList.Destination)
+                .ConstructUsing(src => new Models.IdentityResource())
+                .ReverseMap();
+
+            CreateMap<Entities.IdentityClaim, string>()
+               .ConstructUsing(x => x.Type)
+               .ReverseMap()
+               .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src));
+        }
+    }
+}

+ 43 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/IdentityResourceMappers.cs

@@ -0,0 +1,43 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using AutoMapper;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Extension methods to map to/from entity/model for identity resources.
+    /// </summary>
+    public static class IdentityResourceMappers
+    {
+        static IdentityResourceMappers()
+        {
+            Mapper = new MapperConfiguration(cfg => cfg.AddProfile<IdentityResourceMapperProfile>())
+                .CreateMapper();
+        }
+
+        internal static IMapper Mapper { get; }
+
+        /// <summary>
+        /// Maps an entity to a model.
+        /// </summary>
+        /// <param name="entity">The entity.</param>
+        /// <returns></returns>
+        public static Models.IdentityResource ToModel(this IdentityResource entity)
+        {
+            return entity == null ? null : Mapper.Map<Models.IdentityResource>(entity);
+        }
+
+        /// <summary>
+        /// Maps a model to an entity.
+        /// </summary>
+        /// <param name="model">The model.</param>
+        /// <returns></returns>
+        public static IdentityResource ToEntity(this Models.IdentityResource model)
+        {
+            return model == null ? null : Mapper.Map<IdentityResource>(model);
+        }
+    }
+}

+ 25 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/PersistedGrantMapperProfile.cs

@@ -0,0 +1,25 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using AutoMapper;
+using Models = IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Defines entity/model mapping for persisted grants.
+    /// </summary>
+    /// <seealso cref="AutoMapper.Profile" />
+    public class PersistedGrantMapperProfile:Profile
+    {
+        /// <summary>
+        /// <see cref="PersistedGrantMapperProfile">
+        /// </see>
+        /// </summary>
+        public PersistedGrantMapperProfile()
+        {
+            CreateMap<Entities.PersistedGrant, Models.PersistedGrant>(MemberList.Destination)
+                .ReverseMap();
+        }
+    }
+}

+ 52 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Mappers/PersistedGrantMappers.cs

@@ -0,0 +1,52 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using AutoMapper;
+using IdentityServer4.Models;
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Mappers
+{
+    /// <summary>
+    /// Extension methods to map to/from entity/model for persisted grants.
+    /// </summary>
+    public static class PersistedGrantMappers
+    {
+        static PersistedGrantMappers()
+        {
+            Mapper = new MapperConfiguration(cfg =>cfg.AddProfile<PersistedGrantMapperProfile>())
+                .CreateMapper();
+        }
+
+        internal static IMapper Mapper { get; }
+
+        /// <summary>
+        /// Maps an entity to a model.
+        /// </summary>
+        /// <param name="entity">The entity.</param>
+        /// <returns></returns>
+        public static PersistedGrant ToModel(this Entities.PersistedGrant entity)
+        {
+            return entity == null ? null : Mapper.Map<PersistedGrant>(entity);
+        }
+
+        /// <summary>
+        /// Maps a model to an entity.
+        /// </summary>
+        /// <param name="model">The model.</param>
+        /// <returns></returns>
+        public static Entities.PersistedGrant ToEntity(this PersistedGrant model)
+        {
+            return model == null ? null : Mapper.Map<Entities.PersistedGrant>(model);
+        }
+
+        /// <summary>
+        /// Updates an entity from a model.
+        /// </summary>
+        /// <param name="model">The model.</param>
+        /// <param name="entity">The entity.</param>
+        public static void UpdateEntity(this PersistedGrant model, Entities.PersistedGrant entity)
+        {
+            Mapper.Map(model, entity);
+        }
+    }
+}

+ 33 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/NopLogger.cs

@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage
+{
+    class NopLogger : ILogger, IDisposable
+    {
+        public IDisposable BeginScope<TState>(TState state)
+        {
+            return this;
+        }
+
+        public void Dispose()
+        {
+        }
+
+        public bool IsEnabled(LogLevel logLevel)
+        {
+            return false;
+        }
+
+        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+        {
+        }
+    }
+
+    class NopLogger<T> : NopLogger, ILogger<T>
+    {
+    }
+}

+ 175 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/ConfigurationStoreOptions.cs

@@ -0,0 +1,175 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    /// <summary>
+    /// Options for configuring the configuration context.
+    /// </summary>
+    public class ConfigurationStoreOptions
+    {
+        /// <summary>
+        /// Callback to configure the EF DbContext.
+        /// </summary>
+        /// <value>
+        /// The configure database context.
+        /// </value>
+        public Action<DbContextOptionsBuilder> ConfigureDbContext { get; set; }
+
+        /// <summary>
+        /// Callback in DI resolve the EF DbContextOptions. If set, ConfigureDbContext will not be used.
+        /// </summary>
+        /// <value>
+        /// The configure database context.
+        /// </value>
+        public Action<IServiceProvider, DbContextOptionsBuilder> ResolveDbContextOptions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the default schema.
+        /// </summary>
+        /// <value>
+        /// The default schema.
+        /// </value>
+        public string DefaultSchema { get; set; } = null;
+
+        /// <summary>
+        /// Gets or sets the identity resource table configuration.
+        /// </summary>
+        /// <value>
+        /// The identity resource.
+        /// </value>
+        public TableConfiguration IdentityResource { get; set; } = new TableConfiguration("IdentityResources");
+        /// <summary>
+        /// Gets or sets the identity claim table configuration.
+        /// </summary>
+        /// <value>
+        /// The identity claim.
+        /// </value>
+        public TableConfiguration IdentityClaim { get; set; } = new TableConfiguration("IdentityClaims");
+
+        /// <summary>
+        /// Gets or sets the API resource table configuration.
+        /// </summary>
+        /// <value>
+        /// The API resource.
+        /// </value>
+        public TableConfiguration ApiResource { get; set; } = new TableConfiguration("ApiResources");
+        /// <summary>
+        /// Gets or sets the API secret table configuration.
+        /// </summary>
+        /// <value>
+        /// The API secret.
+        /// </value>
+        public TableConfiguration ApiSecret { get; set; } = new TableConfiguration("ApiSecrets");
+        /// <summary>
+        /// Gets or sets the API scope table configuration.
+        /// </summary>
+        /// <value>
+        /// The API scope.
+        /// </value>
+        public TableConfiguration ApiScope { get; set; } = new TableConfiguration("ApiScopes");
+        /// <summary>
+        /// Gets or sets the API claim table configuration.
+        /// </summary>
+        /// <value>
+        /// The API claim.
+        /// </value>
+        public TableConfiguration ApiClaim { get; set; } = new TableConfiguration("ApiClaims");
+        /// <summary>
+        /// Gets or sets the API scope claim table configuration.
+        /// </summary>
+        /// <value>
+        /// The API scope claim.
+        /// </value>
+        public TableConfiguration ApiScopeClaim { get; set; } = new TableConfiguration("ApiScopeClaims");
+
+        /// <summary>
+        /// Gets or sets the client table configuration.
+        /// </summary>
+        /// <value>
+        /// The client.
+        /// </value>
+        public TableConfiguration Client { get; set; } = new TableConfiguration("Clients");
+        /// <summary>
+        /// Gets or sets the type of the client grant table configuration.
+        /// </summary>
+        /// <value>
+        /// The type of the client grant.
+        /// </value>
+        public TableConfiguration ClientGrantType { get; set; } = new TableConfiguration("ClientGrantTypes");
+        /// <summary>
+        /// Gets or sets the client redirect URI table configuration.
+        /// </summary>
+        /// <value>
+        /// The client redirect URI.
+        /// </value>
+        public TableConfiguration ClientRedirectUri { get; set; } = new TableConfiguration("ClientRedirectUris");
+        /// <summary>
+        /// Gets or sets the client post logout redirect URI table configuration.
+        /// </summary>
+        /// <value>
+        /// The client post logout redirect URI.
+        /// </value>
+        public TableConfiguration ClientPostLogoutRedirectUri { get; set; } = new TableConfiguration("ClientPostLogoutRedirectUris");
+        /// <summary>
+        /// Gets or sets the client scopes table configuration.
+        /// </summary>
+        /// <value>
+        /// The client scopes.
+        /// </value>
+        public TableConfiguration ClientScopes { get; set; } = new TableConfiguration("ClientScopes");
+        /// <summary>
+        /// Gets or sets the client secret table configuration.
+        /// </summary>
+        /// <value>
+        /// The client secret.
+        /// </value>
+        public TableConfiguration ClientSecret { get; set; } = new TableConfiguration("ClientSecrets");
+        /// <summary>
+        /// Gets or sets the client claim table configuration.
+        /// </summary>
+        /// <value>
+        /// The client claim.
+        /// </value>
+        public TableConfiguration ClientClaim { get; set; } = new TableConfiguration("ClientClaims");
+        /// <summary>
+        /// Gets or sets the client IdP restriction table configuration.
+        /// </summary>
+        /// <value>
+        /// The client IdP restriction.
+        /// </value>
+        public TableConfiguration ClientIdPRestriction { get; set; } = new TableConfiguration("ClientIdPRestrictions");
+        /// <summary>
+        /// Gets or sets the client cors origin table configuration.
+        /// </summary>
+        /// <value>
+        /// The client cors origin.
+        /// </value>
+        public TableConfiguration ClientCorsOrigin { get; set; } = new TableConfiguration("ClientCorsOrigins");
+        /// <summary>
+        /// Gets or sets the client property table configuration.
+        /// </summary>
+        /// <value>
+        /// The client property.
+        /// </value>
+        public TableConfiguration ClientProperty { get; set; } = new TableConfiguration("ClientProperties");
+        /// <summary>
+        /// Gets or sets the API resource property table configuration.
+        /// </summary>
+        /// <value>
+        /// The client property.
+        /// </value>
+        public TableConfiguration ApiResourceProperty { get; set; } = new TableConfiguration("ApiProperties");
+        /// <summary>
+        /// Gets or sets the identity resource property table configuration.
+        /// </summary>
+        /// <value>
+        /// The client property.
+        /// </value>
+        public TableConfiguration IdentityResourceProperty { get; set; } = new TableConfiguration("IdentityProperties");
+    }
+}

+ 12 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/DatabaseOptions.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    public class DatabaseOptions
+    {
+        public string ConnectionString { get; set; } = null;
+    }
+}

+ 81 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/OperationalStoreOptions.cs

@@ -0,0 +1,81 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using Microsoft.EntityFrameworkCore;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    /// <summary>
+    /// Options for configuring the operational context.
+    /// </summary>
+    public class OperationalStoreOptions
+    {
+        /// <summary>
+        /// Callback to configure the EF DbContext.
+        /// </summary>
+        /// <value>
+        /// The configure database context.
+        /// </value>
+        public Action<DbContextOptionsBuilder> ConfigureDbContext { get; set; }
+
+        /// <summary>
+        /// Callback in DI resolve the EF DbContextOptions. If set, ConfigureDbContext will not be used.
+        /// </summary>
+        /// <value>
+        /// The configure database context.
+        /// </value>
+        public Action<IServiceProvider, DbContextOptionsBuilder> ResolveDbContextOptions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the default schema.
+        /// </summary>
+        /// <value>
+        /// The default schema.
+        /// </value>
+        public string DefaultSchema { get; set; } = null;
+
+        /// <summary>
+        /// Gets or sets the persisted grants table configuration.
+        /// </summary>
+        /// <value>
+        /// The persisted grants.
+        /// </value>
+        public TableConfiguration PersistedGrants { get; set; } = new TableConfiguration("PersistedGrants");
+
+        /// <summary>
+        /// Gets or sets the device flow codes table configuration.
+        /// </summary>
+        /// <value>
+        /// The device flow codes.
+        /// </value>
+        public TableConfiguration DeviceFlowCodes { get; set; } = new TableConfiguration("DeviceCodes");
+
+        /// <summary>
+        /// Gets or sets a value indicating whether stale entries will be automatically cleaned up from the database.
+        /// This is implemented by perodically connecting to the database (according to the TokenCleanupInterval) from the hosting application.
+        /// Defaults to false.
+        /// </summary>
+        /// <value>
+        ///   <c>true</c> if [enable token cleanup]; otherwise, <c>false</c>.
+        /// </value>
+        public bool EnableTokenCleanup { get; set; } = false;
+
+        /// <summary>
+        /// Gets or sets the token cleanup interval (in seconds). The default is 3600 (1 hour).
+        /// </summary>
+        /// <value>
+        /// The token cleanup interval.
+        /// </value>
+        public int TokenCleanupInterval { get; set; } = 3600;
+
+        /// <summary>
+        /// Gets or sets the number of records to remove at a time. Defaults to 100.
+        /// </summary>
+        /// <value>
+        /// The size of the token cleanup batch.
+        /// </value>
+        public int TokenCleanupBatchSize { get; set; } = 100;
+    }
+}

+ 186 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/PersistedGrantStore.cs

@@ -0,0 +1,186 @@
+using HaBook.IES.IdentityServer.AzureTableStorage.Mappers;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Table;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    public class PersistedGrantStore : IPersistedGrantStore
+    {
+        private readonly string _connectionString;
+        private readonly ILogger _logger;
+        private CloudTable _cloudTable;
+
+        public PersistedGrantStore(IOptions<DatabaseOptions> optionsAccessor, ILogger<PersistedGrantStore> logger)
+        {
+            _connectionString = optionsAccessor.Value.ConnectionString;
+            _logger = logger;
+        }
+
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4PersistedGrantStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+
+            return _cloudTable;
+        }
+
+        public async Task StoreAsync(PersistedGrant token)
+        {
+            var persistedGrant = token.ToEntity();
+            _logger.LogDebug("storing persisted grant: {persistedGrant}", persistedGrant);
+            var table = await InitTable();
+            var operation = TableOperation.InsertOrReplace(persistedGrant);
+            var result = await table.ExecuteAsync(operation);
+            _logger.LogDebug("stored {persistedGrantKey} with result {result}", token.Key, result.HttpStatusCode);
+        }
+
+        async Task<Entities.PersistedGrant> GetEntityAsync(string key)
+        {
+            var table = await InitTable();
+            Entities.PersistedGrant model = null;
+            var query = new TableQuery<Entities.PersistedGrant>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, WebUtility.UrlEncode(key)));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    model = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            _logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null);
+            return model;
+        }
+
+        public async Task<PersistedGrant> GetAsync(string key)
+        {
+            var entity = await GetEntityAsync(key);
+            return entity?.ToModel();
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId)
+        {
+            var filter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var persistedGrants = await GetFilteredEntitiesAsync(filter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}", persistedGrantList.Count, subjectId);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId, string clientId)
+        {
+            var subjectFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var clientFilter = TableQuery.GenerateFilterCondition("ClientId", QueryComparisons.Equal, clientId);
+            var combinedFilter = TableQuery.CombineFilters(subjectFilter, TableOperators.And, clientFilter);
+
+            var persistedGrants = await GetFilteredEntitiesAsync(combinedFilter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}, clientId {clientId}", persistedGrantList.Count, subjectId, clientId);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId, string clientId, string type)
+        {
+            var subjectFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var clientFilter = TableQuery.GenerateFilterCondition("ClientId", QueryComparisons.Equal, clientId);
+            var typeFilter = TableQuery.GenerateFilterCondition("Type", QueryComparisons.Equal, type);
+            var combinedFilter = TableQuery.CombineFilters(subjectFilter, TableOperators.And, TableQuery.CombineFilters(clientFilter, TableOperators.And, typeFilter));
+
+            var persistedGrants = await GetFilteredEntitiesAsync(combinedFilter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}, clientId {type}, clientId {type}", persistedGrantList.Count, subjectId, clientId, type);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetFilteredEntitiesAsync(string filter)
+        {
+            var table = await InitTable();
+            var query = new TableQuery<Entities.PersistedGrant>().Where(filter);
+            TableContinuationToken continuationToken = null;
+            var persistedGrants = new List<Entities.PersistedGrant>();
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                persistedGrants.AddRange(result.Results);
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            return persistedGrants;
+        }
+
+        public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
+        {
+            var entities = await GetAllEntitiesAsync(subjectId);
+            return entities.Select(e => e.ToModel());
+        }
+
+        public async Task RemoveAsync(string key)
+        {
+            var persistedGrant = await GetEntityAsync(key);
+            if (persistedGrant == null)
+            {
+                _logger.LogDebug("no {persistedGrantKey} persisted grant found in database", key);
+                return;
+            }
+
+            var table = await InitTable();
+            var operation = TableOperation.Delete(persistedGrant);
+            var result = await table.ExecuteAsync(operation);
+            _logger.LogDebug("removed {persistedGrantKey} from database with result {result}", key, result.HttpStatusCode);
+        }
+
+        public async Task RemoveAllAsync(string subjectId, string clientId)
+        {
+            var persistedGrants = await GetAllEntitiesAsync(subjectId, clientId);
+            var persistedGrantList = persistedGrants.ToList();
+            _logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}", persistedGrantList.Count, subjectId, clientId);
+
+            await RemoveAllAsync(persistedGrantList);
+        }
+
+        public async Task RemoveAllAsync(string subjectId, string clientId, string type)
+        {
+            var persistedGrants = await GetAllEntitiesAsync(subjectId, clientId, type);
+            var persistedGrantList = persistedGrants.ToList();
+            _logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}", persistedGrantList.Count, subjectId, clientId, type);
+
+            await RemoveAllAsync(persistedGrantList);
+        }
+
+        private async Task RemoveAllAsync(IEnumerable<Entities.PersistedGrant> persistedGrants)
+        {
+            var table = await InitTable();
+
+            foreach (var persistedGrant in persistedGrants)
+            {
+                var operation = TableOperation.Delete(persistedGrant);
+                var result = await table.ExecuteAsync(operation);
+                _logger.LogDebug("removed {persistedGrantKey} from database with result {result}", persistedGrant.PartitionKey,
+                    result.HttpStatusCode);
+            }
+        }
+    }
+}

+ 48 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/TableConfiguration.cs

@@ -0,0 +1,48 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    /// <summary>
+    /// Class to control a table's name and schema.
+    /// </summary>
+    public class TableConfiguration
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TableConfiguration"/> class.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        public TableConfiguration(string name)
+        {
+            Name = name;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="TableConfiguration"/> class.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <param name="schema">The schema.</param>
+        public TableConfiguration(string name, string schema)
+        {
+            Name = name;
+            Schema = schema;
+        }
+
+        /// <summary>
+        /// Gets or sets the name.
+        /// </summary>
+        /// <value>
+        /// The name.
+        /// </value>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the schema.
+        /// </summary>
+        /// <value>
+        /// The schema.
+        /// </value>
+        public string Schema { get; set; }
+    }
+}

+ 12 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Options/TokenCleanupOptions.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Options
+{
+    public class TokenCleanupOptions
+    {
+        public int Interval { get; set; } = 60;
+    }
+}

+ 9 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/About.cshtml

@@ -0,0 +1,9 @@
+@page
+@model AboutModel
+@{
+    ViewData["Title"] = "About";
+}
+<h2>@ViewData["Title"]</h2>
+<h3>@Model.Message</h3>
+
+<p>Use this area to provide additional information.</p>

+ 18 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/About.cshtml.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+{
+    public class AboutModel : PageModel
+    {
+        public string Message { get; set; }
+
+        public void OnGet()
+        {
+            Message = "Your application description page.";
+        }
+    }
+}

+ 19 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Contact.cshtml

@@ -0,0 +1,19 @@
+@page
+@model ContactModel
+@{
+    ViewData["Title"] = "Contact";
+}
+<h2>@ViewData["Title"]</h2>
+<h3>@Model.Message</h3>
+
+<address>
+    One Microsoft Way<br />
+    Redmond, WA 98052-6399<br />
+    <abbr title="Phone">P:</abbr>
+    425.555.0100
+</address>
+
+<address>
+    <strong>Support:</strong> <a href="mailto:Support@example.com">Support@example.com</a><br />
+    <strong>Marketing:</strong> <a href="mailto:Marketing@example.com">Marketing@example.com</a>
+</address>

+ 18 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Contact.cshtml.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+{
+    public class ContactModel : PageModel
+    {
+        public string Message { get; set; }
+
+        public void OnGet()
+        {
+            Message = "Your contact page.";
+        }
+    }
+}

+ 23 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Error.cshtml

@@ -0,0 +1,23 @@
+@page
+@model ErrorModel
+@{
+    ViewData["Title"] = "Error";
+}
+
+<h1 class="text-danger">Error.</h1>
+<h2 class="text-danger">An error occurred while processing your request.</h2>
+
+@if (Model.ShowRequestId)
+{
+    <p>
+        <strong>Request ID:</strong> <code>@Model.RequestId</code>
+    </p>
+}
+
+<h3>Development Mode</h3>
+<p>
+    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
+</p>
+<p>
+    <strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
+</p>

+ 23 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Error.cshtml.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+{
+    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+    public class ErrorModel : PageModel
+    {
+        public string RequestId { get; set; }
+
+        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+        public void OnGet()
+        {
+            RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
+        }
+    }
+}

+ 96 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Index.cshtml

@@ -0,0 +1,96 @@
+@page
+@model IndexModel
+@{
+    ViewData["Title"] = "Home page";
+}
+
+<div id="myCarousel" class="carousel slide" data-ride="carousel" data-interval="6000">
+    <ol class="carousel-indicators">
+        <li data-target="#myCarousel" data-slide-to="0" class="active"></li>
+        <li data-target="#myCarousel" data-slide-to="1"></li>
+        <li data-target="#myCarousel" data-slide-to="2"></li>
+    </ol>
+    <div class="carousel-inner" role="listbox">
+        <div class="item active">
+            <img src="~/images/banner1.svg" alt="ASP.NET" class="img-responsive" />
+            <div class="carousel-caption" role="option">
+                <p>
+                    Learn how to build ASP.NET apps that can run anywhere.
+                    <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkID=525028&clcid=0x409">
+                        Learn More
+                    </a>
+                </p>
+            </div>
+        </div>
+        <div class="item">
+            <img src="~/images/banner2.svg" alt="Visual Studio" class="img-responsive" />
+            <div class="carousel-caption" role="option">
+                <p>
+                    There are powerful new features in Visual Studio for building modern web apps.
+                    <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkID=525030&clcid=0x409">
+                        Learn More
+                    </a>
+                </p>
+            </div>
+        </div>
+        <div class="item">
+            <img src="~/images/banner3.svg" alt="Microsoft Azure" class="img-responsive" />
+            <div class="carousel-caption" role="option">
+                <p>
+                    Learn how Microsoft's Azure cloud platform allows you to build, deploy, and scale web apps.
+                    <a class="btn btn-default" href="https://go.microsoft.com/fwlink/?LinkID=525027&clcid=0x409">
+                        Learn More
+                    </a>
+                </p>
+            </div>
+        </div>
+    </div>
+    <a class="left carousel-control" href="#myCarousel" role="button" data-slide="prev">
+        <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
+        <span class="sr-only">Previous</span>
+    </a>
+    <a class="right carousel-control" href="#myCarousel" role="button" data-slide="next">
+        <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
+        <span class="sr-only">Next</span>
+    </a>
+</div>
+
+<div class="row">
+    <div class="col-md-3">
+        <h2>Application uses</h2>
+        <ul>
+            <li>Sample pages using ASP.NET Core Razor Pages</li>
+            <li>Theming using <a href="https://go.microsoft.com/fwlink/?LinkID=398939">Bootstrap</a></li>
+        </ul>
+    </div>
+    <div class="col-md-3">
+        <h2>How to</h2>
+        <ul>
+            <li><a href="https://go.microsoft.com/fwlink/?linkid=852130">Working with Razor Pages.</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=699315">Manage User Secrets using Secret Manager.</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=699316">Use logging to log a message.</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=699317">Add packages using NuGet.</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=699319">Target development, staging or production environment.</a></li>
+        </ul>
+    </div>
+    <div class="col-md-3">
+        <h2>Overview</h2>
+        <ul>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=518008">Conceptual overview of what is ASP.NET Core</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=699320">Fundamentals of ASP.NET Core such as Startup and middleware.</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=398602">Working with Data</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkId=398603">Security</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=699321">Client side development</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=699322">Develop on different platforms</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=699323">Read more on the documentation site</a></li>
+        </ul>
+    </div>
+    <div class="col-md-3">
+        <h2>Run &amp; Deploy</h2>
+        <ul>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=517851">Run your app</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=517853">Run tools such as EF migrations and more</a></li>
+            <li><a href="https://go.microsoft.com/fwlink/?LinkID=398609">Publish to Microsoft Azure App Service</a></li>
+        </ul>
+    </div>
+</div>

+ 17 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Index.cshtml.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+{
+    public class IndexModel : PageModel
+    {
+        public void OnGet()
+        {
+
+        }
+    }
+}

+ 8 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Privacy.cshtml

@@ -0,0 +1,8 @@
+@page
+@model PrivacyModel
+@{
+    ViewData["Title"] = "Privacy Policy";
+}
+<h2>@ViewData["Title"]</h2>
+
+<p>Use this page to detail your site's privacy policy.</p>

+ 16 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Privacy.cshtml.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+{
+    public class PrivacyModel : PageModel
+    {
+        public void OnGet()
+        {
+        }
+    }
+}

+ 41 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_CookieConsentPartial.cshtml

@@ -0,0 +1,41 @@
+@using Microsoft.AspNetCore.Http.Features
+
+@{
+    var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
+    var showBanner = !consentFeature?.CanTrack ?? false;
+    var cookieString = consentFeature?.CreateConsentCookie();
+}
+
+@if (showBanner)
+{
+    <nav id="cookieConsent" class="navbar navbar-default navbar-fixed-top" role="alert">
+        <div class="container">
+            <div class="navbar-header">
+                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#cookieConsent .navbar-collapse">
+                    <span class="sr-only">Toggle cookie consent banner</span>
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                </button>
+                <span class="navbar-brand"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></span>
+            </div>
+            <div class="collapse navbar-collapse">
+                <p class="navbar-text">
+                    Use this space to summarize your privacy and cookie use policy.
+                </p>
+                <div class="navbar-right">
+                    <a asp-page="/Privacy" class="btn btn-info navbar-btn">Learn More</a>
+                    <button type="button" class="btn btn-default navbar-btn" data-cookie-string="@cookieString">Accept</button>
+                </div>
+            </div>
+        </div>
+    </nav>
+    <script>
+        (function () {
+            document.querySelector("#cookieConsent button[data-cookie-string]").addEventListener("click", function (el) {
+                document.cookie = el.target.dataset.cookieString;
+                document.querySelector("#cookieConsent").classList.add("hidden");
+            }, false);
+        })();
+    </script>
+}

+ 74 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_Layout.cshtml

@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>@ViewData["Title"] - HaBook.IES.IdentityServer.AzureTableStorage</title>
+
+    <environment include="Development">
+        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
+        <link rel="stylesheet" href="~/css/site.css" />
+    </environment>
+    <environment exclude="Development">
+        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
+              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
+              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
+        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
+    </environment>
+</head>
+<body>
+    <nav class="navbar navbar-inverse navbar-fixed-top">
+        <div class="container">
+            <div class="navbar-header">
+                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+                    <span class="sr-only">Toggle navigation</span>
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                    <span class="icon-bar"></span>
+                </button>
+                <a asp-page="/Index" class="navbar-brand">HaBook.IES.IdentityServer.AzureTableStorage</a>
+            </div>
+            <div class="navbar-collapse collapse">
+                <ul class="nav navbar-nav">
+                    <li><a asp-page="/Index">Home</a></li>
+                    <li><a asp-page="/About">About</a></li>
+                    <li><a asp-page="/Contact">Contact</a></li>
+                </ul>
+            </div>
+        </div>
+    </nav>
+
+    <partial name="_CookieConsentPartial" />
+
+    <div class="container body-content">
+        @RenderBody()
+        <hr />
+        <footer>
+            <p>&copy; 2018 - HaBook.IES.IdentityServer.AzureTableStorage</p>
+        </footer>
+    </div>
+
+    <environment include="Development">
+        <script src="~/lib/jquery/dist/jquery.js"></script>
+        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
+        <script src="~/js/site.js" asp-append-version="true"></script>
+    </environment>
+    <environment exclude="Development">
+        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
+                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
+                asp-fallback-test="window.jQuery"
+                crossorigin="anonymous"
+                integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
+        </script>
+        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
+                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
+                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
+                crossorigin="anonymous"
+                integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
+        </script>
+        <script src="~/js/site.min.js" asp-append-version="true"></script>
+    </environment>
+
+    @RenderSection("Scripts", required: false)
+</body>
+</html>

+ 18 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/Shared/_ValidationScriptsPartial.cshtml

@@ -0,0 +1,18 @@
+<environment include="Development">
+    <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
+    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
+</environment>
+<environment exclude="Development">
+    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.17.0/jquery.validate.min.js"
+            asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
+            asp-fallback-test="window.jQuery && window.jQuery.validator"
+            crossorigin="anonymous"
+            integrity="sha384-rZfj/ogBloos6wzLGpPkkOr/gpkBNLZ6b6yLy4o+ok+t/SAKlL5mvXLr0OXNi1Hp">
+    </script>
+    <script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.9/jquery.validate.unobtrusive.min.js"
+            asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
+            asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
+            crossorigin="anonymous"
+            integrity="sha384-ifv0TYDWxBHzvAk2Z0n8R434FL1Rlv/Av18DXE43N/1rvHyOG4izKst0f2iSLdds">
+    </script>
+</environment>

+ 3 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/_ViewImports.cshtml

@@ -0,0 +1,3 @@
+@using HaBook.IES.IdentityServer.AzureTableStorage
+@namespace HaBook.IES.IdentityServer.AzureTableStorage.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

+ 3 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Pages/_ViewStart.cshtml

@@ -0,0 +1,3 @@
+@{
+    Layout = "_Layout";
+}

+ 24 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Program.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            CreateWebHostBuilder(args).Build().Run();
+        }
+
+        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
+            WebHost.CreateDefaultBuilder(args)
+                .UseStartup<Startup>();
+    }
+}

+ 27 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Properties/launchSettings.json

@@ -0,0 +1,27 @@
+{
+  "iisSettings": {
+    "windowsAuthentication": false, 
+    "anonymousAuthentication": true, 
+    "iisExpress": {
+      "applicationUrl": "http://localhost:54425",
+      "sslPort": 44320
+    }
+  },
+  "profiles": {
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "HaBook.IES.IdentityServer.AzureTableStorage": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "applicationUrl": "https://localhost:5001;http://localhost:5000",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 77 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Startup.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Configuration;
+using HaBook.IES.IdentityServer.AzureTableStorage.Extensions;
+using IdentityServer4.Validation;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpsPolicy;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage
+{
+    public class Startup
+    {
+        public Startup(IConfiguration configuration)
+        {
+            Configuration = configuration;
+        }
+
+        public IConfiguration Configuration { get; }
+
+        // This method gets called by the runtime. Use this method to add services to the container.
+        public void ConfigureServices(IServiceCollection services)
+        {
+         
+
+            services.Configure<CookiePolicyOptions>(options =>
+            {
+                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
+                options.CheckConsentNeeded = context => true;
+                options.MinimumSameSitePolicy = SameSiteMode.None;
+            });
+
+            var connectionString = "DefaultEndpointsProtocol=https;AccountName=teammodelstorage;AccountKey=Yq7D4dE6cFuer2d2UZIccTA/i0c3sJ/6ITc8tNOyW+K5f+/lWw9GCos3Mxhj47PyWQgDL8YbVD63B9XcGtrMxQ==;EndpointSuffix=core.chinacloudapi.cn";
+            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
+            services.AddIdentityServer(
+                options =>
+                {
+                    options.Events.RaiseErrorEvents = true;
+                    options.Events.RaiseInformationEvents = true;
+                    options.Events.RaiseFailureEvents = true;
+                    options.Events.RaiseSuccessEvents = true;
+                })
+              .AddTestUsers(TestUsers.Users)
+              .AddOperationalStore(connectionString)
+              .AddConfigurationStore(connectionString);
+
+
+        }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime)
+        {
+            if (env.IsDevelopment())
+            {
+                app.UseDeveloperExceptionPage();
+            }
+            else
+            {
+                app.UseExceptionHandler("/Error");
+                app.UseHsts();
+            }
+
+            app.UseHttpsRedirection();
+            app.UseStaticFiles();
+            app.UseCookiePolicy();
+            //ÆôÓÃIdentityServer
+            app.UseIdentityServer();
+            app.UseMvcWithDefaultRoute();
+        }
+    }
+}

+ 103 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/ClientStore.cs

@@ -0,0 +1,103 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Mappers;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Table;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Stores
+{
+    /// <summary>
+    /// Implementation of IClientStore thats uses EF.
+    /// </summary>
+    /// <seealso cref="IdentityServer4.Stores.IClientStore" />
+    public class ClientStore : IClientStore
+    {
+        //private readonly IConfigurationDbContext _context;
+        private readonly ILogger<ClientStore> _logger;
+        private readonly string _connectionString;
+        private CloudTable _cloudTable;
+        
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ClientStore"/> class.
+        /// </summary>
+        /// <param name="context">The context.</param>
+        /// <param name="logger">The logger.</param>
+        /// <exception cref="ArgumentNullException">context</exception>
+        public ClientStore(
+           // IConfigurationDbContext context,
+           IOptions<DatabaseOptions> optionsAccessor,
+            ILogger<ClientStore> logger)
+        {
+            // _context = context ?? throw new ArgumentNullException(nameof(context));
+            _connectionString = optionsAccessor.Value.ConnectionString;
+            _logger = logger;
+        }
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4ClientStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+
+            return _cloudTable;
+        }
+        /// <summary>
+        /// Finds a client by id
+        /// </summary>
+        /// <param name="clientId">The client id</param>
+        /// <returns>
+        /// The client
+        /// </returns>
+        public async Task<Client> FindClientByIdAsync(string clientId)
+        {
+            // TODO处理业务逻辑
+            var table = await InitTable();
+
+            Entities.Client entity = null;
+            var query = new TableQuery<Entities.Client>().Where(TableQuery.GenerateFilterCondition("ClientId", QueryComparisons.Equal, clientId));
+            TableContinuationToken continuationToken = null;
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                if (result.Results.Count > 0)
+                {
+                    entity = result.Results[0];
+                    break;
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            //处理子表业务
+            // TODO实体业务逻辑
+            //_logger.LogDebug("{clientId} found in database: {IdentityServer4ClientStore}", clientId, entity != null);
+            //var client = _context.Clients
+            //    .Include(x => x.AllowedGrantTypes)
+            //    .Include(x => x.RedirectUris)
+            //    .Include(x => x.PostLogoutRedirectUris)
+            //    .Include(x => x.AllowedScopes)
+            //    .Include(x => x.ClientSecrets)
+            //    .Include(x => x.Claims)
+            //    .Include(x => x.IdentityProviderRestrictions)
+            //    .Include(x => x.AllowedCorsOrigins)
+            //    .Include(x => x.Properties)
+            //    .AsNoTracking()
+            //    .FirstOrDefault(x => x.ClientId == clientId);
+            var model = entity?.ToModel();
+            _logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, model != null);
+            return await Task.FromResult(model);
+        }
+    }
+}

+ 296 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/DeviceFlowStore.cs

@@ -0,0 +1,296 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using IdentityModel;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using IdentityServer4.Stores.Serialization;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.WindowsAzure.Storage.Table;
+using Microsoft.WindowsAzure.Storage;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Stores
+{
+    /// <summary>
+    /// Implementation of IDeviceFlowStore thats uses EF.
+    /// </summary>
+    /// <seealso cref="IdentityServer4.Stores.IDeviceFlowStore" />
+    public class DeviceFlowStore : IDeviceFlowStore
+    {
+       // private readonly IPersistedGrantDbContext _context;
+        private readonly IPersistentGrantSerializer _serializer;
+        private readonly ILogger _logger;
+        private readonly string _connectionString;
+        private CloudTable _cloudTable;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DeviceFlowStore"/> class.
+        /// </summary>
+        /// <param name="context">The context.</param>
+        /// <param name="serializer">The serializer</param>
+        /// <param name="logger">The logger.</param>
+        public DeviceFlowStore(
+            IOptions<DatabaseOptions> optionsAccessor,
+            // IPersistedGrantDbContext context, 
+            IPersistentGrantSerializer serializer, 
+            ILogger<DeviceFlowStore> logger)
+        {
+          //  _context = context;
+            _serializer = serializer;
+            _connectionString = optionsAccessor.Value.ConnectionString;
+            _logger = logger;
+        }
+
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4DeviceFlowStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+            return _cloudTable;
+        }
+
+        /// <summary>
+        /// Stores the device authorization request.
+        /// </summary>
+        /// <param name="deviceCode">The device code.</param>
+        /// <param name="userCode">The user code.</param>
+        /// <param name="data">The data.</param>
+        /// <returns></returns>
+        public async Task StoreDeviceAuthorizationAsync(string deviceCode, string userCode, DeviceCode data)
+        {
+            // TODO处理业务逻辑
+            //保存信息
+            DeviceFlowCodes deviceFlowCodes = ToEntity(data, deviceCode, userCode);
+
+            _logger.LogDebug("storing persisted grant: {DeviceFlowCodes}", deviceFlowCodes);
+            var table = await InitTable();
+            var operation = TableOperation.InsertOrReplace(deviceFlowCodes);
+            var result = await table.ExecuteAsync(operation);
+            // _context.DeviceFlowCodes.Add(ToEntity(data, deviceCode, userCode));
+            //_context.SaveChanges();
+           // return Task.FromResult(0);
+        }
+
+        /// <summary>
+        /// Finds device authorization by user code.
+        /// </summary>
+        /// <param name="userCode">The user code.</param>
+        /// <returns></returns>
+        public async Task<DeviceCode> FindByUserCodeAsync(string userCode)
+        {
+            // TODO处理业务逻辑
+            //查找
+
+            var table = await InitTable();
+            Entities.DeviceFlowCodes deviceFlowCodes = null;
+            var query = new TableQuery<Entities.DeviceFlowCodes>().Where(TableQuery.GenerateFilterCondition("UserCode", QueryComparisons.Equal, userCode));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    deviceFlowCodes = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+
+
+            //var deviceFlowCodes = _context.DeviceFlowCodes.AsNoTracking().FirstOrDefault(x => x.UserCode == userCode);
+            var  model = ToModel(deviceFlowCodes?.Data);
+
+            _logger.LogDebug("{userCode} found in database: {userCodeFound}", userCode, model != null);
+
+            return await Task.FromResult(model);
+        }
+
+        /// <summary>
+        /// Finds device authorization by device code.
+        /// </summary>
+        /// <param name="deviceCode">The device code.</param>
+        /// <returns></returns>
+        public async Task<DeviceCode> FindByDeviceCodeAsync(string deviceCode)
+        {
+            // TODO处理业务逻辑
+            //查找
+
+
+            var table = await InitTable();
+            Entities.DeviceFlowCodes deviceFlowCodes = null;
+            var query = new TableQuery<Entities.DeviceFlowCodes>().Where(TableQuery.GenerateFilterCondition("DeviceCode", QueryComparisons.Equal, deviceCode));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    deviceFlowCodes = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+
+           // var deviceFlowCodes = _context.DeviceFlowCodes.AsNoTracking().FirstOrDefault(x => x.DeviceCode == deviceCode);
+            var model = ToModel(deviceFlowCodes?.Data);
+
+            _logger.LogDebug("{deviceCode} found in database: {deviceCodeFound}", deviceCode, model != null);
+
+            return await Task.FromResult(model);
+        }
+
+        /// <summary>
+        /// Updates device authorization, searching by user code.
+        /// </summary>
+        /// <param name="userCode">The user code.</param>
+        /// <param name="data">The data.</param>
+        /// <returns></returns>
+        public async Task UpdateByUserCodeAsync(string userCode, DeviceCode data)
+        {
+            // TODO处理业务逻辑
+            //查找
+           // var existing = _context.DeviceFlowCodes.SingleOrDefault(x => x.UserCode == userCode);
+
+            var table = await InitTable();
+            Entities.DeviceFlowCodes deviceFlowCodes = null;
+            var query = new TableQuery<Entities.DeviceFlowCodes>().Where(TableQuery.GenerateFilterCondition("UserCode", QueryComparisons.Equal, userCode));
+            TableContinuationToken continuationToken = null;
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    deviceFlowCodes = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            if (deviceFlowCodes == null)
+            {
+                _logger.LogError("{userCode} not found in database", userCode);
+                throw new InvalidOperationException("Could not update device code");
+            }
+
+            var entity = ToEntity(data, deviceFlowCodes.DeviceCode, userCode);
+            _logger.LogDebug("{userCode} found in database", userCode);
+
+            deviceFlowCodes.SubjectId = data.Subject?.FindFirst(JwtClaimTypes.Subject).Value;
+            deviceFlowCodes.Data = entity.Data;
+
+            try
+            {
+
+                _logger.LogDebug("storing persisted grant: {DeviceFlowCodes}", deviceFlowCodes);
+                 // table = await InitTable();
+                var operation = TableOperation.InsertOrReplace(deviceFlowCodes);
+                var result = await table.ExecuteAsync(operation);
+              //  _context.SaveChanges();
+            }
+            catch (DbUpdateConcurrencyException ex)
+            {
+                _logger.LogWarning("exception updating {userCode} user code in database: {error}", userCode, ex.Message);
+            }
+
+          //  return await Task.FromResult(0);
+        }
+
+        /// <summary>
+        /// Removes the device authorization, searching by device code.
+        /// </summary>
+        /// <param name="deviceCode">The device code.</param>
+        /// <returns></returns>
+        public async Task RemoveByDeviceCodeAsync(string deviceCode)
+        {
+            //var deviceFlowCodes = _context.DeviceFlowCodes.FirstOrDefault(x => x.DeviceCode == deviceCode);
+
+            var table = await InitTable();
+            Entities.DeviceFlowCodes deviceFlowCodes = null;
+            var query = new TableQuery<Entities.DeviceFlowCodes>().Where(TableQuery.GenerateFilterCondition("DeviceCode", QueryComparisons.Equal, deviceCode));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    deviceFlowCodes = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            if (deviceFlowCodes != null)
+            {
+                _logger.LogDebug("removing {deviceCode} device code from database", deviceCode);
+
+              //  _context.DeviceFlowCodes.Remove(deviceFlowCodes);
+
+                try
+                {
+                    //   _context.SaveChanges();
+                    table = await InitTable();
+                    var operation = TableOperation.Delete(deviceFlowCodes);
+                    var result = await table.ExecuteAsync(operation);
+                }
+                catch (DbUpdateConcurrencyException ex)
+                {
+                    _logger.LogInformation("exception removing {deviceCode} device code from database: {error}", deviceCode, ex.Message);
+                }
+            }
+            else
+            {
+                _logger.LogDebug("no {deviceCode} device code found in database", deviceCode);
+            }
+
+         //   return Task.FromResult(0);
+        }
+
+        private DeviceFlowCodes ToEntity(DeviceCode model, string deviceCode, string userCode)
+        {
+            if (model == null || deviceCode == null || userCode == null) return null;
+
+            return new DeviceFlowCodes
+            {
+                DeviceCode = deviceCode,
+                UserCode = userCode,
+                ClientId = model.ClientId,
+                SubjectId = model.Subject?.FindFirst(JwtClaimTypes.Subject).Value,
+                CreationTime = model.CreationTime,
+                Expiration = model.CreationTime.AddSeconds(model.Lifetime),
+                Data = _serializer.Serialize(model)
+            };
+        }
+
+        private DeviceCode ToModel(string entity)
+        {
+            if (entity == null) return null;
+
+            return _serializer.Deserialize<DeviceCode>(entity);
+        }
+    }
+}

+ 336 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/PersistedGrantStore.cs

@@ -0,0 +1,336 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using HaBook.IES.IdentityServer.AzureTableStorage.Mappers;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using Microsoft.Extensions.Logging;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.WindowsAzure.Storage.Table;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage;
+using System.Net;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Stores
+{
+    /// <summary>
+    /// Implementation of IPersistedGrantStore thats uses EF.
+    /// </summary>
+    /// <seealso cref="IdentityServer4.Stores.IPersistedGrantStore" />
+    public class PersistedGrantStore : IPersistedGrantStore
+    {
+        private readonly string _connectionString;
+       // private readonly IPersistedGrantDbContext _context;
+        private readonly ILogger _logger;
+        private CloudTable _cloudTable;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PersistedGrantStore"/> class.
+        /// </summary>
+        /// <param name="context">The context.</param>
+        /// <param name="logger">The logger.</param>
+        public PersistedGrantStore(IOptions<DatabaseOptions> optionsAccessor, ILogger<PersistedGrantStore> logger)
+        {
+            _connectionString = optionsAccessor.Value.ConnectionString;
+           // _context = context;
+            _logger = logger;
+        }
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4PersistedGrantStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+
+            return _cloudTable;
+        }
+        /// <summary>
+        /// Stores the asynchronous.
+        /// </summary>
+        /// <param name="token">The token.</param>
+        /// <returns></returns>
+        public async Task StoreAsync(PersistedGrant token)
+        {
+            /**
+            var existing = _context.PersistedGrants.SingleOrDefault(x => x.Key == token.Key);
+            if (existing == null)
+            {
+                _logger.LogDebug("{persistedGrantKey} not found in database", token.Key);
+
+                var persistedGrant = token.ToEntity();
+                _context.PersistedGrants.Add(persistedGrant);
+            }
+            else
+            {
+                _logger.LogDebug("{persistedGrantKey} found in database", token.Key);
+
+                token.UpdateEntity(existing);
+            }
+
+            try
+            {
+                _context.SaveChanges();
+            }
+            catch (DbUpdateConcurrencyException ex)
+            {
+                _logger.LogWarning("exception updating {persistedGrantKey} persisted grant in database: {error}", token.Key, ex.Message);
+            }
+
+            return Task.FromResult(0);
+        **/
+            var persistedGrant = token.ToEntity();
+            _logger.LogDebug("storing persisted grant: {persistedGrant}", persistedGrant);
+            var table = await InitTable();
+            var operation = TableOperation.InsertOrReplace(persistedGrant);
+            var result = await table.ExecuteAsync(operation);
+            _logger.LogDebug("stored {persistedGrantKey} with result {result}", token.Key, result.HttpStatusCode);
+        }
+
+        async Task<Entities.PersistedGrant> GetEntityAsync(string key)
+        {
+            var table = await InitTable();
+            Entities.PersistedGrant model = null;
+            var query = new TableQuery<Entities.PersistedGrant>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, WebUtility.UrlEncode(key)));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    model = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            _logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null);
+            return model;
+        }
+
+        /// <summary>
+        /// Gets the grant.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns></returns>
+        public async Task<PersistedGrant> GetAsync(string key)
+        {
+            //var persistedGrant = _context.PersistedGrants.AsNoTracking().FirstOrDefault(x => x.Key == key);
+            // var model = persistedGrant?.ToModel();
+            var entity = await GetEntityAsync(key);
+            //_logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null);
+
+            return entity.ToModel();
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId)
+        {
+            var filter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var persistedGrants = await GetFilteredEntitiesAsync(filter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}", persistedGrantList.Count, subjectId);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId, string clientId)
+        {
+            var subjectFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var clientFilter = TableQuery.GenerateFilterCondition("ClientId", QueryComparisons.Equal, clientId);
+            var combinedFilter = TableQuery.CombineFilters(subjectFilter, TableOperators.And, clientFilter);
+
+            var persistedGrants = await GetFilteredEntitiesAsync(combinedFilter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}, clientId {clientId}", persistedGrantList.Count, subjectId, clientId);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetAllEntitiesAsync(string subjectId, string clientId, string type)
+        {
+            var subjectFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, subjectId);
+            var clientFilter = TableQuery.GenerateFilterCondition("ClientId", QueryComparisons.Equal, clientId);
+            var typeFilter = TableQuery.GenerateFilterCondition("Type", QueryComparisons.Equal, type);
+            var combinedFilter = TableQuery.CombineFilters(subjectFilter, TableOperators.And, TableQuery.CombineFilters(clientFilter, TableOperators.And, typeFilter));
+
+            var persistedGrants = await GetFilteredEntitiesAsync(combinedFilter);
+            var persistedGrantList = persistedGrants.ToList();
+
+            _logger.LogDebug("{persistedGrantCount} persisted grants found for subjectId {subjectId}, clientId {type}, clientId {type}", persistedGrantList.Count, subjectId, clientId, type);
+            return persistedGrantList;
+        }
+
+        async Task<IEnumerable<Entities.PersistedGrant>> GetFilteredEntitiesAsync(string filter)
+        {
+            var table = await InitTable();
+            var query = new TableQuery<Entities.PersistedGrant>().Where(filter);
+            TableContinuationToken continuationToken = null;
+            var persistedGrants = new List<Entities.PersistedGrant>();
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                persistedGrants.AddRange(result.Results);
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            return persistedGrants;
+        }
+
+        /// <summary>
+        /// Gets all grants for a given subject id.
+        /// </summary>
+        /// <param name="subjectId">The subject identifier.</param>
+        /// <returns></returns>
+        public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
+        {
+            //var persistedGrants = _context.PersistedGrants.Where(x => x.SubjectId == subjectId).AsNoTracking().ToList();
+            //var model = persistedGrants.Select(x => x.ToModel());
+
+            //_logger.LogDebug("{persistedGrantCount} persisted grants found for {subjectId}", persistedGrants.Count, subjectId);
+
+            //return Task.FromResult(model);
+            var entities = await GetAllEntitiesAsync(subjectId);
+            return entities.Select(e => e.ToModel());
+        }
+
+        /// <summary>
+        /// Removes the grant by key.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <returns></returns>
+        public async Task RemoveAsync(string key)
+        {
+            //var persistedGrant = _context.PersistedGrants.FirstOrDefault(x => x.Key == key);
+            //if (persistedGrant!= null)
+            //{
+            //    _logger.LogDebug("removing {persistedGrantKey} persisted grant from database", key);
+
+            //    _context.PersistedGrants.Remove(persistedGrant);
+
+            //    try
+            //    {
+            //        _context.SaveChanges();
+            //    }
+            //    catch(DbUpdateConcurrencyException ex)
+            //    {
+            //        _logger.LogInformation("exception removing {persistedGrantKey} persisted grant from database: {error}", key, ex.Message);
+            //    }
+            //}
+            //else
+            //{
+            //    _logger.LogDebug("no {persistedGrantKey} persisted grant found in database", key);
+            //}
+
+            //return Task.FromResult(0);
+
+            var persistedGrant = await GetEntityAsync(key);
+            if (persistedGrant == null)
+            {
+                _logger.LogDebug("no {persistedGrantKey} persisted grant found in database", key);
+                return;
+            }
+
+            var table = await InitTable();
+            var operation = TableOperation.Delete(persistedGrant);
+            var result = await table.ExecuteAsync(operation);
+            _logger.LogDebug("removed {persistedGrantKey} from database with result {result}", key, result.HttpStatusCode);
+        }
+
+
+
+        /// <summary>
+        /// Removes all grants for a given subject id and client id combination.
+        /// </summary>
+        /// <param name="subjectId">The subject identifier.</param>
+        /// <param name="clientId">The client identifier.</param>
+        /// <returns></returns>
+        public async Task RemoveAllAsync(string subjectId, string clientId)
+        {
+            //var persistedGrants = _context.PersistedGrants.Where(x => x.SubjectId == subjectId && x.ClientId == clientId).ToList();
+
+            //_logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}", persistedGrants.Count, subjectId, clientId);
+
+            //_context.PersistedGrants.RemoveRange(persistedGrants);
+
+            //try
+            //{
+            //    _context.SaveChanges();
+            //}
+            //catch (DbUpdateConcurrencyException ex)
+            //{
+            //    _logger.LogInformation("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}: {error}", persistedGrants.Count, subjectId, clientId, ex.Message);
+            //}
+
+            //return Task.FromResult(0);
+
+
+            var persistedGrants = await GetAllEntitiesAsync(subjectId, clientId);
+            var persistedGrantList = persistedGrants.ToList();
+            _logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}", persistedGrantList.Count, subjectId, clientId);
+
+            await RemoveAllAsync(persistedGrantList);
+        }
+
+        /// <summary>
+        /// Removes all grants of a give type for a given subject id and client id combination.
+        /// </summary>
+        /// <param name="subjectId">The subject identifier.</param>
+        /// <param name="clientId">The client identifier.</param>
+        /// <param name="type">The type.</param>
+        /// <returns></returns>
+        public async Task RemoveAllAsync(string subjectId, string clientId, string type)
+        {
+            /**
+            var persistedGrants = _context.PersistedGrants.Where(x =>
+                x.SubjectId == subjectId &&
+                x.ClientId == clientId &&
+                x.Type == type).ToList();
+
+            _logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}", persistedGrants.Count, subjectId, clientId, type);
+
+            _context.PersistedGrants.RemoveRange(persistedGrants);
+
+            try
+            {
+                _context.SaveChanges();
+            }
+            catch (DbUpdateConcurrencyException ex)
+            {
+                _logger.LogInformation("exception removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}: {error}", persistedGrants.Count, subjectId, clientId, type, ex.Message);
+            }
+
+            return Task.FromResult(0);
+            **/
+            var persistedGrants = await GetAllEntitiesAsync(subjectId, clientId, type);
+            var persistedGrantList = persistedGrants.ToList();
+            _logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}", persistedGrantList.Count, subjectId, clientId, type);
+
+            await RemoveAllAsync(persistedGrantList);
+        }
+
+
+        private async Task RemoveAllAsync(IEnumerable<Entities.PersistedGrant> persistedGrants)
+        {
+            var table = await InitTable();
+
+            foreach (var persistedGrant in persistedGrants)
+            {
+                var operation = TableOperation.Delete(persistedGrant);
+                var result = await table.ExecuteAsync(operation);
+                _logger.LogDebug("removed {persistedGrantKey} from database with result {result}", persistedGrant.PartitionKey,
+                    result.HttpStatusCode);
+            }
+        }
+    }
+}

+ 293 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/Stores/ResourceStore.cs

@@ -0,0 +1,293 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using HaBook.IES.IdentityServer.AzureTableStorage.Mappers;
+using IdentityServer4.Models;
+using IdentityServer4.Stores;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.WindowsAzure.Storage.Table;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage;
+using Entities=  HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using System.Threading;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Stores
+{
+    /// <summary>
+    /// Implementation of IResourceStore thats uses EF.
+    /// </summary>
+    /// <seealso cref="IResourceStore" />
+    public class ResourceStore : IResourceStore
+    {
+
+        
+        //private readonly IConfigurationDbContext _context;
+        private readonly ILogger<ResourceStore> _logger;
+        private readonly string _connectionString;
+        private CloudTable _cloudTable;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ResourceStore"/> class.
+        /// </summary>
+        /// <param name="context">The context.</param>
+        /// <param name="logger">The logger.</param>
+        /// <exception cref="ArgumentNullException">context</exception>
+        public ResourceStore(IOptions<DatabaseOptions> optionsAccessor, ILogger<ResourceStore> logger)
+        {
+            _connectionString = optionsAccessor.Value.ConnectionString;
+           // _context = context ?? throw new ArgumentNullException(nameof(context));
+            _logger = logger;
+        }
+
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4ApiResourceStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+
+            return _cloudTable;
+        }
+
+
+        /// <summary>
+        /// Finds the API resource by name.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns></returns>
+        public async Task<ApiResource> FindApiResourceAsync(string name)
+        {
+            //处理实体业务
+            /*
+            var query =
+                from apiResource in _context.ApiResources
+                where apiResource.Name == name
+                select apiResource;
+
+            var apis = query
+                .Include(x => x.Secrets)
+                .Include(x => x.Scopes)
+                    .ThenInclude(s => s.UserClaims)
+                .Include(x => x.UserClaims)
+                .Include(x => x.Properties)
+                .AsNoTracking();
+
+            var api = apis.FirstOrDefault();
+
+            if (api != null)
+            {
+                _logger.LogDebug("Found {api} API resource in database", name);
+            }
+            else
+            {
+                _logger.LogDebug("Did not find {api} API resource in database", name);
+            }
+
+            return Task.FromResult(api.ToModel());
+            */
+
+            var table = await InitTable();
+            Entities.ApiResource api = null;
+            var query = new TableQuery<Entities.ApiResource>().Where(TableQuery.GenerateFilterCondition("Name", QueryComparisons.Equal,name));
+            TableContinuationToken continuationToken = null;
+
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+
+                if (result.Results.Count > 0)
+                {
+                    api = result.Results[0];
+                    break;
+                }
+
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            return await  Task.FromResult(api.ToModel());
+
+        }
+
+        /// <summary>
+        /// Gets API resources by scope name.
+        /// </summary>
+        /// <param name="scopeNames"></param>
+        /// <returns></returns>
+        public async  Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames)
+        {
+            /*
+
+            var names = scopeNames.ToArray();
+
+            var query =
+                from api in _context.ApiResources
+                where api.Scopes.Where(x=>names.Contains(x.Name)).Any()
+                select api;
+
+            var apis = query
+                .Include(x => x.Secrets)
+                .Include(x => x.Scopes)
+                    .ThenInclude(s => s.UserClaims)
+                .Include(x => x.UserClaims)
+                .Include(x => x.Properties)
+                .AsNoTracking();
+
+            var results = apis.ToArray();
+            var models = results.Select(x => x.ToModel()).ToArray();
+
+            _logger.LogDebug("Found {scopes} API scopes in database", models.SelectMany(x => x.Scopes).Select(x => x.Name));
+
+            return Task.FromResult(models.AsEnumerable());
+            */
+            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_connectionString);
+            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
+            CloudTable table = tableClient.GetTableReference("IdentityServer4ApiResourceStore");
+            CloudTable scope_table = tableClient.GetTableReference("IdentityServer4ApiScopeStore");
+            Action<IList<Entities.ApiScope>> onProgress = null;
+            CancellationToken ct = default(CancellationToken);
+            var scopes = new List<Entities.ApiScope>();
+            TableContinuationToken token = null;
+            foreach (string name in scopeNames) {
+                do
+                {
+                    var query = new TableQuery<Entities.ApiScope>().Where(TableQuery.GenerateFilterCondition("Name", QueryComparisons.Equal, name));
+                    TableQuerySegment<Entities.ApiScope> seg = await  scope_table.ExecuteQuerySegmentedAsync<Entities.ApiScope>(query, token);
+                    token = seg.ContinuationToken;
+                    scopes.AddRange(seg);
+                    onProgress?.Invoke(scopes);
+
+                } while (token != null && !ct.IsCancellationRequested);
+            }
+            var items = new List<Entities.ApiResource>();
+            TableContinuationToken sctoken = null;
+            Action<IList<Entities.ApiResource>> sconProgress = null;
+            foreach (Entities.ApiScope scope in scopes)
+            {
+                do
+                {
+                    var query = new TableQuery<Entities.ApiResource>().Where(TableQuery.GenerateFilterCondition("Id", QueryComparisons.Equal, scope.ApiResourceId+""));
+                    TableQuerySegment<Entities.ApiResource> seg = await scope_table.ExecuteQuerySegmentedAsync<Entities.ApiResource>(query, sctoken);
+                    sctoken = seg.ContinuationToken;
+                    items.AddRange(seg);
+                    sconProgress?.Invoke(items);
+
+                } while (sctoken != null && !ct.IsCancellationRequested);
+            }
+            var models = items.Select(x => x.ToModel()).ToArray();
+            return await Task.FromResult(models.AsEnumerable());
+            }
+      
+
+        /// <summary>
+        /// Gets identity resources by scope name.
+        /// </summary>
+        /// <param name="scopeNames"></param>
+        /// <returns></returns>
+        public async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames)
+        {
+            var scopes = scopeNames.ToArray();
+            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_connectionString);
+            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
+            CloudTable table = tableClient.GetTableReference("IdentityServer4IdentityResourceStore");
+            //var query =
+            //        from identityResource in _context.IdentityResources
+            //        where scopes.Contains(identityResource.Name)
+            //        select identityResource;
+
+            //var resources = query
+            //        .Include(x => x.UserClaims)
+            //        .Include(x => x.Properties)
+            //        .AsNoTracking();
+            TableQuery<Entities.IdentityResource> query = new TableQuery<Entities.IdentityResource>();
+            TableContinuationToken continuationToken = null;
+            List<Entities.IdentityResource> resources = null;
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                if (result.Results.Count > 0)
+                {
+                        resources = result.ToList();
+                    break;
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+            resources.Where<Entities.IdentityResource>(x => scopeNames.Contains<string>(x.Name) );
+            var results = resources.ToArray();
+            _logger.LogDebug("Found {scopes} identity scopes in database", results.Select(x => x.Name));
+            return await Task.FromResult(results.Select(x => x.ToModel()).ToArray().AsEnumerable());
+        }
+
+        /// <summary>
+        /// Gets all resources.
+        /// </summary>
+        /// <returns></returns>
+        public async Task<Resources> GetAllResourcesAsync()
+        {
+            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_connectionString);
+            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
+            CloudTable table = tableClient.GetTableReference("IdentityServer4IdentityResourceStore");
+
+            TableQuery<Entities.IdentityResource> query = new TableQuery<Entities.IdentityResource>();
+            TableContinuationToken continuationToken = null;
+            List<Entities.IdentityResource> identity = null;
+            TableQuerySegment<Entities.IdentityResource> result = null;
+            do
+            {
+                result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                if (result.Results.Count > 0)
+                {
+                    identity = result.ToList();
+                    break;
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+
+             
+            CloudTable api_table = tableClient.GetTableReference("IdentityServer4ApiResourcesStore");
+
+            TableQuery<Entities.ApiResource> api_query = new TableQuery<Entities.ApiResource>();
+            TableContinuationToken api_continuationToken = null;
+            List<Entities.ApiResource> apis = null;
+            TableQuerySegment<Entities.ApiResource> api_result = null;
+            do
+            {
+                api_result = await api_table.ExecuteQuerySegmentedAsync(api_query, api_continuationToken);
+                if (api_result.Results.Count > 0)
+                {
+                    apis = api_result.ToList();
+                    break;
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+
+
+
+            //var identity = _context.IdentityResources
+            //  .Include(x => x.UserClaims);
+
+            //var apis = _context.ApiResources
+            //    .Include(x => x.Secrets)
+            //    .Include(x => x.Scopes)
+            //        .ThenInclude(s => s.UserClaims)
+            //    .Include(x => x.UserClaims)
+            //    .Include(x => x.Properties)
+            //    .AsNoTracking();
+            var resources = new Resources(
+                identity.ToArray().Select(x => x.ToModel()).AsEnumerable(),
+                apis.ToArray().Select(x => x.ToModel()).AsEnumerable());
+            _logger.LogDebug("Found {scopes} as all scopes in database", resources.IdentityResources.Select(x=>x.Name).Union(resources.ApiResources.SelectMany(x=>x.Scopes).Select(x=>x.Name)));
+            return await Task.FromResult(resources);
+        }
+    }
+}

+ 23 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/IOperationalStoreNotification.cs

@@ -0,0 +1,23 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage
+{
+    /// <summary>
+    /// Interface to model notifications from the TokenCleanup feature.
+    /// </summary>
+    public interface IOperationalStoreNotification
+    {
+        /// <summary>
+        /// Notification for persisted grants being removed.
+        /// </summary>
+        /// <param name="persistedGrants"></param>
+        /// <returns></returns>
+        Task PersistedGrantsRemovedAsync(IEnumerable<PersistedGrant> persistedGrants);
+    }
+}

+ 107 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/Services/CorsPolicyService.cs

@@ -0,0 +1,107 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Threading.Tasks;
+using IdentityServer4.Services;
+using System.Linq;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.WindowsAzure.Storage.Table;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.Extensions.Options;
+using Microsoft.WindowsAzure.Storage;
+using HaBook.IES.IdentityServer.AzureTableStorage.Entities;
+using System.Collections.Generic;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage.Services
+{
+    /// <summary>
+    /// Implementation of ICorsPolicyService that consults the client configuration in the database for allowed CORS origins.
+    /// </summary>
+    /// <seealso cref="IdentityServer4.Services.ICorsPolicyService" />
+    public class CorsPolicyService : ICorsPolicyService
+    {
+        //private readonly IHttpContextAccessor _context;
+        private readonly ILogger<CorsPolicyService> _logger;
+        private readonly string _connectionString;
+        private CloudTable _cloudTable;
+        private CloudTable client_cloudTable;
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CorsPolicyService"/> class.
+        /// </summary>
+        /// <param name="context">The context.</param>
+        /// <param name="logger">The logger.</param>
+        /// <exception cref="ArgumentNullException">context</exception>
+        public CorsPolicyService(IOptions<DatabaseOptions> optionsAccessor, ILogger<CorsPolicyService> logger)
+        {
+            _connectionString = optionsAccessor.Value.ConnectionString;
+            //_context = context ?? throw new ArgumentNullException(nameof(context));
+            _logger = logger;
+        }
+        async Task<CloudTable> InitTable()
+        {
+            if (_cloudTable != null) return _cloudTable;
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            _cloudTable = tableClient.GetTableReference("IdentityServer4ClientCorsOriginStore");
+            await _cloudTable.CreateIfNotExistsAsync();
+
+            return _cloudTable;
+        }
+
+        async Task<CloudTable> Client_InitTable()
+        {
+            if (client_cloudTable != null) return client_cloudTable;
+            var storageAccount = CloudStorageAccount.Parse(_connectionString);
+            var tableClient = storageAccount.CreateCloudTableClient();
+            client_cloudTable = tableClient.GetTableReference("IdentityServer4ClientStore");
+            await client_cloudTable.CreateIfNotExistsAsync();
+
+            return client_cloudTable;
+        }
+
+        /// <summary>
+        /// Determines whether origin is allowed.
+        /// </summary>
+        /// <param name="origin">The origin.</param>
+        /// <returns></returns>
+        public async Task<bool> IsOriginAllowedAsync(string origin)
+        {
+
+            TableQuery<Client> query = new TableQuery<Client>();
+            var table = await Client_InitTable();
+            TableContinuationToken continuationToken = null;
+            List<Client> origins=null ;
+            
+            do
+            {
+                var result = await table.ExecuteQuerySegmentedAsync(query, continuationToken);
+                if (result.Results.Count > 0)
+                {
+                    origins = result.ToList() ;
+                    break;
+                }
+                continuationToken = result.ContinuationToken;
+            } while (continuationToken != null);
+
+            // doing this here and not in the ctor because: https://github.com/aspnet/CORS/issues/105
+           // var dbContext = _context.HttpContext.RequestServices.GetRequiredService<IConfigurationDbContext>();
+
+            //var origins = dbContext.Clients.SelectMany(x => x.AllowedCorsOrigins.Select(y => y.Origin)).AsNoTracking().ToList();
+
+            //处理子表业务
+            var originsStr = origins.SelectMany(x => x.AllowedCorsOrigins.Select(y => y.Origin)).ToList();
+            var distinctOrigins = originsStr.Where(x => x != null).Distinct();
+
+            var isAllowed = distinctOrigins.Contains(origin, StringComparer.OrdinalIgnoreCase);
+
+            _logger.LogDebug("Origin {origin} is allowed: {originAllowed}", origin, isAllowed);
+
+            return await Task.FromResult(isAllowed);
+        }
+    }
+}

+ 176 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/TokenCleanup/TokenCleanup.cs

@@ -0,0 +1,176 @@
+// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using HaBook.IES.IdentityServer.AzureTableStorage.Interfaces;
+using HaBook.IES.IdentityServer.AzureTableStorage.Options;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace HaBook.IES.IdentityServer.AzureTableStorage
+{
+    /// <summary>
+    /// Helper to perodically cleanup expired persisted grants.
+    /// </summary>
+    public class TokenCleanup
+    {
+        private readonly ILogger<TokenCleanup> _logger;
+        private readonly IServiceProvider _serviceProvider;
+        private readonly OperationalStoreOptions _options;
+
+        private CancellationTokenSource _source;
+
+        private TimeSpan CleanupInterval => TimeSpan.FromSeconds(_options.TokenCleanupInterval);
+
+        /// <summary>
+        /// Constructor for TokenCleanup.
+        /// </summary>
+        /// <param name="serviceProvider"></param>
+        /// <param name="logger"></param>
+        /// <param name="options"></param>
+        public TokenCleanup(IServiceProvider serviceProvider, ILogger<TokenCleanup> logger, OperationalStoreOptions options)
+        {
+            _options = options ?? throw new ArgumentNullException(nameof(options));
+            if (_options.TokenCleanupInterval < 1) throw new ArgumentException("Token cleanup interval must be at least 1 second");
+            if (_options.TokenCleanupBatchSize < 1) throw new ArgumentException("Token cleanup batch size interval must be at least 1");
+
+            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
+        }
+
+        /// <summary>
+        /// Starts the token cleanup polling.
+        /// </summary>
+        public void Start()
+        {
+            Start(CancellationToken.None);
+        }
+
+        /// <summary>
+        /// Starts the token cleanup polling.
+        /// </summary>
+        public void Start(CancellationToken cancellationToken)
+        {
+            if (_source != null) throw new InvalidOperationException("Already started. Call Stop first.");
+
+            _logger.LogDebug("Starting grant removal");
+
+            _source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+
+            Task.Factory.StartNew(() => StartInternalAsync(_source.Token));
+        }
+
+        /// <summary>
+        /// Stops the token cleanup polling.
+        /// </summary>
+        public void Stop()
+        {
+            if (_source == null) throw new InvalidOperationException("Not started. Call Start first.");
+
+            _logger.LogDebug("Stopping grant removal");
+
+            _source.Cancel();
+            _source = null;
+        }
+
+        private async Task StartInternalAsync(CancellationToken cancellationToken)
+        {
+            while (true)
+            {
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    _logger.LogDebug("CancellationRequested. Exiting.");
+                    break;
+                }
+
+                try
+                {
+                    await Task.Delay(CleanupInterval, cancellationToken);
+                }
+                catch (TaskCanceledException)
+                {
+                    _logger.LogDebug("TaskCanceledException. Exiting.");
+                    break;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError("Task.Delay exception: {0}. Exiting.", ex.Message);
+                    break;
+                }
+
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    _logger.LogDebug("CancellationRequested. Exiting.");
+                    break;
+                }
+
+                await RemoveExpiredGrantsAsync();
+            }
+        }
+
+        /// <summary>
+        /// Method to clear expired persisted grants.
+        /// </summary>
+        /// <returns></returns>
+        public async Task RemoveExpiredGrantsAsync()
+        {
+            try
+            {
+                _logger.LogTrace("Querying for expired grants to remove");
+
+                //var found = Int32.MaxValue;
+
+                using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
+                {
+                    // TODO处理业务逻辑 
+                    /**
+                    var tokenCleanupNotification = serviceScope.ServiceProvider.GetService<IOperationalStoreNotification>();
+                    using (var context = serviceScope.ServiceProvider.GetService<IPersistedGrantDbContext>())
+                    {
+                        while (found >= _options.TokenCleanupBatchSize)
+                        {
+                            var expired = context.PersistedGrants
+                                .Where(x => x.Expiration < DateTime.UtcNow)
+                                .OrderBy(x => x.Key)
+                                .Take(_options.TokenCleanupBatchSize)
+                                .ToArray();
+
+                            found = expired.Length;
+                            _logger.LogInformation("Removing {grantCount} grants", found);
+
+                            if (found > 0)
+                            {
+                                context.PersistedGrants.RemoveRange(expired);
+                                try
+                                {
+                                    context.SaveChanges();
+
+                                    if (tokenCleanupNotification != null)
+                                    {
+                                        await tokenCleanupNotification.PersistedGrantsRemovedAsync(expired);
+                                    }
+                                }
+                                catch (DbUpdateConcurrencyException ex)
+                                {
+                                    _logger.LogDebug("Concurrency exception removing expired grants: {exception}", ex.Message);
+                                }
+                            }
+                        }
+                    }
+                    **/
+
+                    throw new NotImplementedException();
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError("Exception removing expired grants: {exception}", ex.Message);
+            }
+        }
+    }
+}

+ 9 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/appsettings.Development.json

@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Debug",
+      "System": "Information",
+      "Microsoft": "Information"
+    }
+  }
+}

+ 8 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/appsettings.json

@@ -0,0 +1,8 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Warning"
+    }
+  },
+  "AllowedHosts": "*"
+}

+ 37 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/css/site.css

@@ -0,0 +1,37 @@
+/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 
+for details on configuring this project to bundle and minify static web assets. */
+body {
+    padding-top: 50px;
+    padding-bottom: 20px;
+}
+
+/* Wrapping element */
+/* Set some basic padding to keep content from hitting the edges */
+.body-content {
+    padding-left: 15px;
+    padding-right: 15px;
+}
+
+/* Carousel */
+.carousel-caption p {
+    font-size: 20px;
+    line-height: 1.4;
+}
+
+/* Make .svg files in the carousel display properly in older browsers */
+.carousel-inner .item img[src$=".svg"] {
+    width: 100%;
+}
+
+/* QR code generator */
+#qrCode {
+    margin: 15px;
+}
+
+/* Hide/rearrange for smaller screens */
+@media screen and (max-width: 767px) {
+    /* Hide captions */
+    .carousel-caption {
+        display: none;
+    }
+}

+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/css/site.min.css

@@ -0,0 +1 @@
+body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}}

BIN
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/favicon.ico


File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner1.svg


File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner2.svg


File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/images/banner3.svg


+ 4 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/js/site.js

@@ -0,0 +1,4 @@
+// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
+// for details on configuring this project to bundle and minify static web assets.
+
+// Write your Javascript code.

+ 0 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/js/site.min.js


+ 45 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/.bower.json

@@ -0,0 +1,45 @@
+{
+  "name": "bootstrap",
+  "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
+  "keywords": [
+    "css",
+    "js",
+    "less",
+    "mobile-first",
+    "responsive",
+    "front-end",
+    "framework",
+    "web"
+  ],
+  "homepage": "http://getbootstrap.com",
+  "license": "MIT",
+  "moduleType": "globals",
+  "main": [
+    "less/bootstrap.less",
+    "dist/js/bootstrap.js"
+  ],
+  "ignore": [
+    "/.*",
+    "_config.yml",
+    "CNAME",
+    "composer.json",
+    "CONTRIBUTING.md",
+    "docs",
+    "js/tests",
+    "test-infra"
+  ],
+  "dependencies": {
+    "jquery": "1.9.1 - 3"
+  },
+  "version": "3.3.7",
+  "_release": "3.3.7",
+  "_resolution": {
+    "type": "version",
+    "tag": "v3.3.7",
+    "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86"
+  },
+  "_source": "https://github.com/twbs/bootstrap.git",
+  "_target": "v3.3.7",
+  "_originalSource": "bootstrap",
+  "_direct": true
+}

+ 21 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2011-2016 Twitter, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 587 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css

@@ -0,0 +1,587 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+}
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn-default.disabled,
+.btn-primary.disabled,
+.btn-success.disabled,
+.btn-info.disabled,
+.btn-warning.disabled,
+.btn-danger.disabled,
+.btn-default[disabled],
+.btn-primary[disabled],
+.btn-success[disabled],
+.btn-info[disabled],
+.btn-warning[disabled],
+.btn-danger[disabled],
+fieldset[disabled] .btn-default,
+fieldset[disabled] .btn-primary,
+fieldset[disabled] .btn-success,
+fieldset[disabled] .btn-info,
+fieldset[disabled] .btn-warning,
+fieldset[disabled] .btn-danger {
+  -webkit-box-shadow: none;
+          box-shadow: none;
+}
+.btn-default .badge,
+.btn-primary .badge,
+.btn-success .badge,
+.btn-info .badge,
+.btn-warning .badge,
+.btn-danger .badge {
+  text-shadow: none;
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+.btn-default.disabled,
+.btn-default[disabled],
+fieldset[disabled] .btn-default,
+.btn-default.disabled:hover,
+.btn-default[disabled]:hover,
+fieldset[disabled] .btn-default:hover,
+.btn-default.disabled:focus,
+.btn-default[disabled]:focus,
+fieldset[disabled] .btn-default:focus,
+.btn-default.disabled.focus,
+.btn-default[disabled].focus,
+fieldset[disabled] .btn-default.focus,
+.btn-default.disabled:active,
+.btn-default[disabled]:active,
+fieldset[disabled] .btn-default:active,
+.btn-default.disabled.active,
+.btn-default[disabled].active,
+fieldset[disabled] .btn-default.active {
+  background-color: #e0e0e0;
+  background-image: none;
+}
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #265a88 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88));
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #265a88 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #245580;
+}
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #265a88;
+  background-position: 0 -15px;
+}
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #265a88;
+  border-color: #245580;
+}
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled.focus,
+.btn-primary[disabled].focus,
+fieldset[disabled] .btn-primary.focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+  background-color: #265a88;
+  background-image: none;
+}
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+}
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+.btn-success.disabled,
+.btn-success[disabled],
+fieldset[disabled] .btn-success,
+.btn-success.disabled:hover,
+.btn-success[disabled]:hover,
+fieldset[disabled] .btn-success:hover,
+.btn-success.disabled:focus,
+.btn-success[disabled]:focus,
+fieldset[disabled] .btn-success:focus,
+.btn-success.disabled.focus,
+.btn-success[disabled].focus,
+fieldset[disabled] .btn-success.focus,
+.btn-success.disabled:active,
+.btn-success[disabled]:active,
+fieldset[disabled] .btn-success:active,
+.btn-success.disabled.active,
+.btn-success[disabled].active,
+fieldset[disabled] .btn-success.active {
+  background-color: #419641;
+  background-image: none;
+}
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+}
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+.btn-info.disabled,
+.btn-info[disabled],
+fieldset[disabled] .btn-info,
+.btn-info.disabled:hover,
+.btn-info[disabled]:hover,
+fieldset[disabled] .btn-info:hover,
+.btn-info.disabled:focus,
+.btn-info[disabled]:focus,
+fieldset[disabled] .btn-info:focus,
+.btn-info.disabled.focus,
+.btn-info[disabled].focus,
+fieldset[disabled] .btn-info.focus,
+.btn-info.disabled:active,
+.btn-info[disabled]:active,
+fieldset[disabled] .btn-info:active,
+.btn-info.disabled.active,
+.btn-info[disabled].active,
+fieldset[disabled] .btn-info.active {
+  background-color: #2aabd2;
+  background-image: none;
+}
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+}
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+.btn-warning.disabled,
+.btn-warning[disabled],
+fieldset[disabled] .btn-warning,
+.btn-warning.disabled:hover,
+.btn-warning[disabled]:hover,
+fieldset[disabled] .btn-warning:hover,
+.btn-warning.disabled:focus,
+.btn-warning[disabled]:focus,
+fieldset[disabled] .btn-warning:focus,
+.btn-warning.disabled.focus,
+.btn-warning[disabled].focus,
+fieldset[disabled] .btn-warning.focus,
+.btn-warning.disabled:active,
+.btn-warning[disabled]:active,
+fieldset[disabled] .btn-warning:active,
+.btn-warning.disabled.active,
+.btn-warning[disabled].active,
+fieldset[disabled] .btn-warning.active {
+  background-color: #eb9316;
+  background-image: none;
+}
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+}
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+.btn-danger.disabled,
+.btn-danger[disabled],
+fieldset[disabled] .btn-danger,
+.btn-danger.disabled:hover,
+.btn-danger[disabled]:hover,
+fieldset[disabled] .btn-danger:hover,
+.btn-danger.disabled:focus,
+.btn-danger[disabled]:focus,
+fieldset[disabled] .btn-danger:focus,
+.btn-danger.disabled.focus,
+.btn-danger[disabled].focus,
+fieldset[disabled] .btn-danger.focus,
+.btn-danger.disabled:active,
+.btn-danger[disabled]:active,
+fieldset[disabled] .btn-danger:active,
+.btn-danger.disabled.active,
+.btn-danger[disabled].active,
+fieldset[disabled] .btn-danger.active {
+  background-color: #c12e2a;
+  background-image: none;
+}
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #2e6da4;
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+  background-repeat: repeat-x;
+}
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+}
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
+  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
+}
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+}
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
+  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+}
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
+}
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+@media (max-width: 767px) {
+  .navbar .navbar-nav .open .dropdown-menu > .active > a,
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
+  .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
+    color: #fff;
+    background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+    background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+    background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+    background-repeat: repeat-x;
+  }
+}
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+}
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+}
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+}
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+}
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+}
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #286090 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090));
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #286090 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-striped {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #286090;
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a));
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+}
+.list-group-item.active .badge,
+.list-group-item.active:hover .badge,
+.list-group-item.active:focus .badge {
+  text-shadow: none;
+}
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+}
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image:      -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
+  background-image:         linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+}
+/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map


File diff suppressed because it is too large
+ 6 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css


File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map


File diff suppressed because it is too large
+ 6757 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.css


File diff suppressed because it is too large
+ 1 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map


File diff suppressed because it is too large
+ 6 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css


+ 0 - 0
src/framework/HaBook.IES.IdentityServer.AzureTableStorage/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map


Some files were not shown because too many files changed in this diff