added Microser
This commit is contained in:
88
src/Microser/Microser.IdS/Config.cs
Normal file
88
src/Microser/Microser.IdS/Config.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Microser.IdS
|
||||
{
|
||||
public static class Config
|
||||
{
|
||||
public static IEnumerable<IdentityResource> IdentityResources =>
|
||||
new IdentityResource[]
|
||||
{
|
||||
new IdentityResources.OpenId(),
|
||||
new IdentityResources.Profile(),
|
||||
new IdentityResources.Address(),
|
||||
new IdentityResources.Email(),
|
||||
new IdentityResource(
|
||||
"roles",
|
||||
"Your role(s)",
|
||||
new List<string>(){ JwtClaimTypes.Role })
|
||||
};
|
||||
|
||||
public static IEnumerable<ApiScope> ApiScopes =>
|
||||
new ApiScope[]
|
||||
{
|
||||
new ApiScope("scope1"),
|
||||
new ApiScope("scope2"),
|
||||
|
||||
new ApiScope("microser_api_weather"),
|
||||
};
|
||||
|
||||
public static IEnumerable<Client> Clients =>
|
||||
new Client[]
|
||||
{
|
||||
new Client
|
||||
{
|
||||
ClientId = "dotnet_blazor_serverapp",
|
||||
ClientName = "Blazor Server App",
|
||||
ClientSecrets = {
|
||||
new Secret("E8C65E41BB0E4E519D409023CF5112F4".Sha256())
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.Code,
|
||||
RequirePkce = true,
|
||||
RequireClientSecret = true,
|
||||
AllowedCorsOrigins = { "https://localhost:7001" },
|
||||
AllowedScopes = {
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServerConstants.StandardScopes.Profile,
|
||||
IdentityServerConstants.StandardScopes.Address,
|
||||
IdentityServerConstants.StandardScopes.Email,
|
||||
"roles",
|
||||
"scope1",
|
||||
"microser_api_weather"
|
||||
},
|
||||
RedirectUris = { "https://localhost:7001/signin-oidc" },
|
||||
PostLogoutRedirectUris = { "https://localhost:7001/signout-callback-oidc" },
|
||||
Enabled = true
|
||||
},
|
||||
|
||||
// m2m client credentials flow client
|
||||
new Client
|
||||
{
|
||||
ClientId = "m2m.client",
|
||||
ClientName = "Client Credentials Client",
|
||||
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
|
||||
|
||||
AllowedScopes = { "scope1" }
|
||||
},
|
||||
|
||||
// interactive client using code flow + pkce
|
||||
new Client
|
||||
{
|
||||
ClientId = "interactive",
|
||||
ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
|
||||
|
||||
AllowedGrantTypes = GrantTypes.Code,
|
||||
|
||||
RedirectUris = { "https://localhost:44300/signin-oidc" },
|
||||
FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
|
||||
PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "scope2" }
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
106
src/Microser/Microser.IdS/HostingExtensions.cs
Normal file
106
src/Microser/Microser.IdS/HostingExtensions.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Duende.IdentityServer;
|
||||
using Microser.IdS.Pages.Admin.ApiScopes;
|
||||
using Microser.IdS.Pages.Admin.Clients;
|
||||
using Microser.IdS.Pages.Admin.IdentityScopes;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
|
||||
namespace Microser.IdS
|
||||
{
|
||||
internal static class HostingExtensions
|
||||
{
|
||||
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
|
||||
var isBuilder = builder.Services
|
||||
.AddIdentityServer(options =>
|
||||
{
|
||||
options.Events.RaiseErrorEvents = true;
|
||||
options.Events.RaiseInformationEvents = true;
|
||||
options.Events.RaiseFailureEvents = true;
|
||||
options.Events.RaiseSuccessEvents = true;
|
||||
|
||||
// see https://docs.duendesoftware.com/identityserver/v5/fundamentals/resources/
|
||||
options.EmitStaticAudienceClaim = true;
|
||||
})
|
||||
.AddTestUsers(TestUsers.Users)
|
||||
// this adds the config data from DB (clients, resources, CORS)
|
||||
.AddConfigurationStore(options =>
|
||||
{
|
||||
options.ConfigureDbContext = b =>
|
||||
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
|
||||
})
|
||||
// this is something you will want in production to reduce load on and requests to the DB
|
||||
//.AddConfigurationStoreCache()
|
||||
//
|
||||
// this adds the operational data from DB (codes, tokens, consents)
|
||||
.AddOperationalStore(options =>
|
||||
{
|
||||
options.ConfigureDbContext = b =>
|
||||
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication()
|
||||
.AddGoogle(options =>
|
||||
{
|
||||
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
|
||||
|
||||
// register your IdentityServer with Google at https://console.developers.google.com
|
||||
// enable the Google+ API
|
||||
// set the redirect URI to https://localhost:5001/signin-google
|
||||
options.ClientId = "copy client ID from Google here";
|
||||
options.ClientSecret = "copy client secret from Google here";
|
||||
});
|
||||
|
||||
// this adds the necessary config for the simple admin/config pages
|
||||
{
|
||||
builder.Services.AddAuthorization(options =>
|
||||
options.AddPolicy("admin",
|
||||
policy => policy.RequireClaim("sub", "1"))
|
||||
);
|
||||
|
||||
builder.Services.Configure<RazorPagesOptions>(options =>
|
||||
options.Conventions.AuthorizeFolder("/Admin", "admin"));
|
||||
|
||||
builder.Services.AddTransient<Microser.IdS.Pages.Portal.ClientRepository>();
|
||||
builder.Services.AddTransient<ClientRepository>();
|
||||
builder.Services.AddTransient<IdentityScopeRepository>();
|
||||
builder.Services.AddTransient<ApiScopeRepository>();
|
||||
}
|
||||
|
||||
// if you want to use server-side sessions: https://blog.duendesoftware.com/posts/20220406_session_management/
|
||||
// then enable it
|
||||
//isBuilder.AddServerSideSessions();
|
||||
//
|
||||
// and put some authorization on the admin/management pages using the same policy created above
|
||||
//builder.Services.Configure<RazorPagesOptions>(options =>
|
||||
// options.Conventions.AuthorizeFolder("/ServerSideSessions", "admin"));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static WebApplication ConfigurePipeline(this WebApplication app)
|
||||
{
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseIdentityServer();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages()
|
||||
.RequireAuthorization();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/Microser/Microser.IdS/IdentityServer.db
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db
Normal file
Binary file not shown.
BIN
src/Microser/Microser.IdS/IdentityServer.db-shm
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db-shm
Normal file
Binary file not shown.
BIN
src/Microser/Microser.IdS/IdentityServer.db-wal
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db-wal
Normal file
Binary file not shown.
24
src/Microser/Microser.IdS/Microser.IdS.csproj
Normal file
24
src/Microser/Microser.IdS/Microser.IdS.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.0.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
301
src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql
Normal file
301
src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql
Normal file
@@ -0,0 +1,301 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "ApiResources" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResources" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"AllowedAccessTokenSigningAlgorithms" TEXT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"RequireResourceIndicator" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Required" INTEGER NOT NULL,
|
||||
"Emphasize" INTEGER NOT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Clients" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_Clients" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"ProtocolType" TEXT NOT NULL,
|
||||
"RequireClientSecret" INTEGER NOT NULL,
|
||||
"ClientName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"ClientUri" TEXT NULL,
|
||||
"LogoUri" TEXT NULL,
|
||||
"RequireConsent" INTEGER NOT NULL,
|
||||
"AllowRememberConsent" INTEGER NOT NULL,
|
||||
"AlwaysIncludeUserClaimsInIdToken" INTEGER NOT NULL,
|
||||
"RequirePkce" INTEGER NOT NULL,
|
||||
"AllowPlainTextPkce" INTEGER NOT NULL,
|
||||
"RequireRequestObject" INTEGER NOT NULL,
|
||||
"AllowAccessTokensViaBrowser" INTEGER NOT NULL,
|
||||
"RequireDPoP" INTEGER NOT NULL,
|
||||
"DPoPValidationMode" INTEGER NOT NULL,
|
||||
"DPoPClockSkew" TEXT NOT NULL,
|
||||
"FrontChannelLogoutUri" TEXT NULL,
|
||||
"FrontChannelLogoutSessionRequired" INTEGER NOT NULL,
|
||||
"BackChannelLogoutUri" TEXT NULL,
|
||||
"BackChannelLogoutSessionRequired" INTEGER NOT NULL,
|
||||
"AllowOfflineAccess" INTEGER NOT NULL,
|
||||
"IdentityTokenLifetime" INTEGER NOT NULL,
|
||||
"AllowedIdentityTokenSigningAlgorithms" TEXT NULL,
|
||||
"AccessTokenLifetime" INTEGER NOT NULL,
|
||||
"AuthorizationCodeLifetime" INTEGER NOT NULL,
|
||||
"ConsentLifetime" INTEGER NULL,
|
||||
"AbsoluteRefreshTokenLifetime" INTEGER NOT NULL,
|
||||
"SlidingRefreshTokenLifetime" INTEGER NOT NULL,
|
||||
"RefreshTokenUsage" INTEGER NOT NULL,
|
||||
"UpdateAccessTokenClaimsOnRefresh" INTEGER NOT NULL,
|
||||
"RefreshTokenExpiration" INTEGER NOT NULL,
|
||||
"AccessTokenType" INTEGER NOT NULL,
|
||||
"EnableLocalLogin" INTEGER NOT NULL,
|
||||
"IncludeJwtId" INTEGER NOT NULL,
|
||||
"AlwaysSendClientClaims" INTEGER NOT NULL,
|
||||
"ClientClaimsPrefix" TEXT NULL,
|
||||
"PairWiseSubjectSalt" TEXT NULL,
|
||||
"InitiateLoginUri" TEXT NULL,
|
||||
"UserSsoLifetime" INTEGER NULL,
|
||||
"UserCodeType" TEXT NULL,
|
||||
"DeviceCodeLifetime" INTEGER NOT NULL,
|
||||
"CibaLifetime" INTEGER NULL,
|
||||
"PollingInterval" INTEGER NULL,
|
||||
"CoordinateLifetimeWithUserSession" INTEGER NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL,
|
||||
"PushedAuthorizationLifetime" INTEGER NULL,
|
||||
"RequirePushedAuthorization" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityProviders" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityProviders" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scheme" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Properties" TEXT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResources" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResources" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Required" INTEGER NOT NULL,
|
||||
"Emphasize" INTEGER NOT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceClaims_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceProperties_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scope" TEXT NOT NULL,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceScopes_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceSecrets" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceSecrets" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceSecrets_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopeClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"ScopeId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiScopeClaims_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopeProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ScopeId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiScopeProperties_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientClaims_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientCorsOrigins" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientCorsOrigins" PRIMARY KEY AUTOINCREMENT,
|
||||
"Origin" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientCorsOrigins_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientGrantTypes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientGrantTypes" PRIMARY KEY AUTOINCREMENT,
|
||||
"GrantType" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientGrantTypes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientIdPRestrictions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientIdPRestrictions" PRIMARY KEY AUTOINCREMENT,
|
||||
"Provider" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientIdPRestrictions_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientPostLogoutRedirectUris" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientPostLogoutRedirectUris" PRIMARY KEY AUTOINCREMENT,
|
||||
"PostLogoutRedirectUri" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientPostLogoutRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ClientProperties_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientRedirectUris" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientRedirectUris" PRIMARY KEY AUTOINCREMENT,
|
||||
"RedirectUri" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scope" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientScopes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientSecrets" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientSecrets" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ClientSecrets_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResourceClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"IdentityResourceId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResourceProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"IdentityResourceId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceClaims_ApiResourceId_Type" ON "ApiResourceClaims" ("ApiResourceId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceProperties_ApiResourceId_Key" ON "ApiResourceProperties" ("ApiResourceId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResources_Name" ON "ApiResources" ("Name");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceScopes_ApiResourceId_Scope" ON "ApiResourceScopes" ("ApiResourceId", "Scope");
|
||||
|
||||
CREATE INDEX "IX_ApiResourceSecrets_ApiResourceId" ON "ApiResourceSecrets" ("ApiResourceId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopeClaims_ScopeId_Type" ON "ApiScopeClaims" ("ScopeId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopeProperties_ScopeId_Key" ON "ApiScopeProperties" ("ScopeId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopes_Name" ON "ApiScopes" ("Name");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientClaims_ClientId_Type_Value" ON "ClientClaims" ("ClientId", "Type", "Value");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientCorsOrigins_ClientId_Origin" ON "ClientCorsOrigins" ("ClientId", "Origin");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientGrantTypes_ClientId_GrantType" ON "ClientGrantTypes" ("ClientId", "GrantType");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientIdPRestrictions_ClientId_Provider" ON "ClientIdPRestrictions" ("ClientId", "Provider");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri" ON "ClientPostLogoutRedirectUris" ("ClientId", "PostLogoutRedirectUri");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientProperties_ClientId_Key" ON "ClientProperties" ("ClientId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientRedirectUris_ClientId_RedirectUri" ON "ClientRedirectUris" ("ClientId", "RedirectUri");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Clients_ClientId" ON "Clients" ("ClientId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientScopes_ClientId_Scope" ON "ClientScopes" ("ClientId", "Scope");
|
||||
|
||||
CREATE INDEX "IX_ClientSecrets_ClientId" ON "ClientSecrets" ("ClientId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityProviders_Scheme" ON "IdentityProviders" ("Scheme");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResourceClaims_IdentityResourceId_Type" ON "IdentityResourceClaims" ("IdentityResourceId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResourceProperties_IdentityResourceId_Key" ON "IdentityResourceProperties" ("IdentityResourceId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResources_Name" ON "IdentityResources" ("Name");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240312131641_Configuration', '8.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
1067
src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs
generated
Normal file
1067
src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,721 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.ConfigurationDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Configuration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResources",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireResourceIndicator = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResources", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Clients",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ProtocolType = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
RequireClientSecret = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
ClientUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
LogoUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
RequireConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowRememberConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequirePkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowPlainTextPkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireRequestObject = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowAccessTokensViaBrowser = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireDPoP = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DPoPValidationMode = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DPoPClockSkew = table.Column<TimeSpan>(type: "TEXT", nullable: false),
|
||||
FrontChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
BackChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
BackChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowOfflineAccess = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IdentityTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
AccessTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AuthorizationCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConsentLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SlidingRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
RefreshTokenUsage = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RefreshTokenExpiration = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AccessTokenType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
EnableLocalLogin = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IncludeJwtId = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AlwaysSendClientClaims = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientClaimsPrefix = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
PairWiseSubjectSalt = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
InitiateLoginUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
UserSsoLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
UserCodeType = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
DeviceCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CibaLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
PollingInterval = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
CoordinateLifetimeWithUserSession = table.Column<bool>(type: "INTEGER", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
PushedAuthorizationLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
RequirePushedAuthorization = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Clients", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityProviders",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scheme = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityProviders", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResources",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResources", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceSecrets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopeClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
|
||||
column: x => x.ScopeId,
|
||||
principalTable: "ApiScopes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopeProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
|
||||
column: x => x.ScopeId,
|
||||
principalTable: "ApiScopes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientClaims_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientCorsOrigins",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Origin = table.Column<string>(type: "TEXT", maxLength: 150, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientCorsOrigins_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientGrantTypes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GrantType = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientGrantTypes_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientIdPRestrictions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Provider = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientIdPRestrictions_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientPostLogoutRedirectUris",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
PostLogoutRedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientProperties_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientRedirectUris",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientRedirectUris_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientScopes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientScopes_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientSecrets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientSecrets", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientSecrets_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResourceClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
|
||||
column: x => x.IdentityResourceId,
|
||||
principalTable: "IdentityResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResourceProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId",
|
||||
column: x => x.IdentityResourceId,
|
||||
principalTable: "IdentityResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceClaims_ApiResourceId_Type",
|
||||
table: "ApiResourceClaims",
|
||||
columns: new[] { "ApiResourceId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceProperties_ApiResourceId_Key",
|
||||
table: "ApiResourceProperties",
|
||||
columns: new[] { "ApiResourceId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResources_Name",
|
||||
table: "ApiResources",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceScopes_ApiResourceId_Scope",
|
||||
table: "ApiResourceScopes",
|
||||
columns: new[] { "ApiResourceId", "Scope" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceSecrets_ApiResourceId",
|
||||
table: "ApiResourceSecrets",
|
||||
column: "ApiResourceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopeClaims_ScopeId_Type",
|
||||
table: "ApiScopeClaims",
|
||||
columns: new[] { "ScopeId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopeProperties_ScopeId_Key",
|
||||
table: "ApiScopeProperties",
|
||||
columns: new[] { "ScopeId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopes_Name",
|
||||
table: "ApiScopes",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientClaims_ClientId_Type_Value",
|
||||
table: "ClientClaims",
|
||||
columns: new[] { "ClientId", "Type", "Value" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientCorsOrigins_ClientId_Origin",
|
||||
table: "ClientCorsOrigins",
|
||||
columns: new[] { "ClientId", "Origin" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientGrantTypes_ClientId_GrantType",
|
||||
table: "ClientGrantTypes",
|
||||
columns: new[] { "ClientId", "GrantType" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientIdPRestrictions_ClientId_Provider",
|
||||
table: "ClientIdPRestrictions",
|
||||
columns: new[] { "ClientId", "Provider" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri",
|
||||
table: "ClientPostLogoutRedirectUris",
|
||||
columns: new[] { "ClientId", "PostLogoutRedirectUri" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientProperties_ClientId_Key",
|
||||
table: "ClientProperties",
|
||||
columns: new[] { "ClientId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientRedirectUris_ClientId_RedirectUri",
|
||||
table: "ClientRedirectUris",
|
||||
columns: new[] { "ClientId", "RedirectUri" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Clients_ClientId",
|
||||
table: "Clients",
|
||||
column: "ClientId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientScopes_ClientId_Scope",
|
||||
table: "ClientScopes",
|
||||
columns: new[] { "ClientId", "Scope" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientSecrets_ClientId",
|
||||
table: "ClientSecrets",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityProviders_Scheme",
|
||||
table: "IdentityProviders",
|
||||
column: "Scheme",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResourceClaims_IdentityResourceId_Type",
|
||||
table: "IdentityResourceClaims",
|
||||
columns: new[] { "IdentityResourceId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResourceProperties_IdentityResourceId_Key",
|
||||
table: "IdentityResourceProperties",
|
||||
columns: new[] { "IdentityResourceId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResources_Name",
|
||||
table: "IdentityResources",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopeClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopeProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientCorsOrigins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientGrantTypes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientIdPRestrictions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientPostLogoutRedirectUris");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientRedirectUris");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityProviders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResourceClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResourceProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Clients");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResources");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
99
src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql
Normal file
99
src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql
Normal file
@@ -0,0 +1,99 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "DeviceCodes" (
|
||||
"UserCode" TEXT NOT NULL CONSTRAINT "PK_DeviceCodes" PRIMARY KEY,
|
||||
"DeviceCode" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"CreationTime" TEXT NOT NULL,
|
||||
"Expiration" TEXT NOT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Keys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Keys" PRIMARY KEY,
|
||||
"Version" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Use" TEXT NULL,
|
||||
"Algorithm" TEXT NOT NULL,
|
||||
"IsX509Certificate" INTEGER NOT NULL,
|
||||
"DataProtected" INTEGER NOT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "PersistedGrants" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_PersistedGrants" PRIMARY KEY AUTOINCREMENT,
|
||||
"Key" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"CreationTime" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"ConsumedTime" TEXT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "PushedAuthorizationRequests" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_PushedAuthorizationRequests" PRIMARY KEY AUTOINCREMENT,
|
||||
"ReferenceValueHash" TEXT NOT NULL,
|
||||
"ExpiresAtUtc" TEXT NOT NULL,
|
||||
"Parameters" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ServerSideSessions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ServerSideSessions" PRIMARY KEY AUTOINCREMENT,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Scheme" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NOT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Renewed" TEXT NOT NULL,
|
||||
"Expires" TEXT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_DeviceCodes_DeviceCode" ON "DeviceCodes" ("DeviceCode");
|
||||
|
||||
CREATE INDEX "IX_DeviceCodes_Expiration" ON "DeviceCodes" ("Expiration");
|
||||
|
||||
CREATE INDEX "IX_Keys_Use" ON "Keys" ("Use");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_ConsumedTime" ON "PersistedGrants" ("ConsumedTime");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_Expiration" ON "PersistedGrants" ("Expiration");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_PersistedGrants_Key" ON "PersistedGrants" ("Key");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_SubjectId_ClientId_Type" ON "PersistedGrants" ("SubjectId", "ClientId", "Type");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_SubjectId_SessionId_Type" ON "PersistedGrants" ("SubjectId", "SessionId", "Type");
|
||||
|
||||
CREATE INDEX "IX_PushedAuthorizationRequests_ExpiresAtUtc" ON "PushedAuthorizationRequests" ("ExpiresAtUtc");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_PushedAuthorizationRequests_ReferenceValueHash" ON "PushedAuthorizationRequests" ("ReferenceValueHash");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_DisplayName" ON "ServerSideSessions" ("DisplayName");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_Expires" ON "ServerSideSessions" ("Expires");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ServerSideSessions_Key" ON "ServerSideSessions" ("Key");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_SessionId" ON "ServerSideSessions" ("SessionId");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_SubjectId" ON "ServerSideSessions" ("SubjectId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240312131625_Grants', '8.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
259
src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs
generated
Normal file
259
src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs
generated
Normal file
@@ -0,0 +1,259 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
[DbContext(typeof(PersistedGrantDbContext))]
|
||||
[Migration("20240312131625_Grants")]
|
||||
partial class Grants
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
b.Property<string>("UserCode")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserCode");
|
||||
|
||||
b.HasIndex("DeviceCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.ToTable("DeviceCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Algorithm")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DataProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsX509Certificate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Use")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Use");
|
||||
|
||||
b.ToTable("Keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ConsumedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ConsumedTime");
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||
|
||||
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||
|
||||
b.ToTable("PersistedGrants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceValueHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAtUtc");
|
||||
|
||||
b.HasIndex("ReferenceValueHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PushedAuthorizationRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Renewed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scheme")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayName");
|
||||
|
||||
b.HasIndex("Expires");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.ToTable("ServerSideSessions", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Grants : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DeviceCodes",
|
||||
columns: table => new
|
||||
{
|
||||
UserCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DeviceCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Keys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Version = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Use = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Algorithm = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
IsX509Certificate = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DataProtected = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Keys", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PersistedGrants",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
ConsumedTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PersistedGrants", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PushedAuthorizationRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ReferenceValueHash = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Parameters = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PushedAuthorizationRequests", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerSideSessions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Scheme = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Renewed = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expires = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerSideSessions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceCodes_DeviceCode",
|
||||
table: "DeviceCodes",
|
||||
column: "DeviceCode",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceCodes_Expiration",
|
||||
table: "DeviceCodes",
|
||||
column: "Expiration");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Keys_Use",
|
||||
table: "Keys",
|
||||
column: "Use");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_ConsumedTime",
|
||||
table: "PersistedGrants",
|
||||
column: "ConsumedTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_Expiration",
|
||||
table: "PersistedGrants",
|
||||
column: "Expiration");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_Key",
|
||||
table: "PersistedGrants",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||
table: "PersistedGrants",
|
||||
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
|
||||
table: "PersistedGrants",
|
||||
columns: new[] { "SubjectId", "SessionId", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PushedAuthorizationRequests_ExpiresAtUtc",
|
||||
table: "PushedAuthorizationRequests",
|
||||
column: "ExpiresAtUtc");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PushedAuthorizationRequests_ReferenceValueHash",
|
||||
table: "PushedAuthorizationRequests",
|
||||
column: "ReferenceValueHash",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_DisplayName",
|
||||
table: "ServerSideSessions",
|
||||
column: "DisplayName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_Expires",
|
||||
table: "ServerSideSessions",
|
||||
column: "Expires");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_Key",
|
||||
table: "ServerSideSessions",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_SessionId",
|
||||
table: "ServerSideSessions",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_SubjectId",
|
||||
table: "ServerSideSessions",
|
||||
column: "SubjectId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DeviceCodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Keys");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PersistedGrants");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PushedAuthorizationRequests");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerSideSessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
[DbContext(typeof(PersistedGrantDbContext))]
|
||||
partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
b.Property<string>("UserCode")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserCode");
|
||||
|
||||
b.HasIndex("DeviceCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.ToTable("DeviceCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Algorithm")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DataProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsX509Certificate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Use")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Use");
|
||||
|
||||
b.ToTable("Keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ConsumedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ConsumedTime");
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||
|
||||
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||
|
||||
b.ToTable("PersistedGrants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceValueHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAtUtc");
|
||||
|
||||
b.HasIndex("ReferenceValueHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PushedAuthorizationRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Renewed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scheme")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayName");
|
||||
|
||||
b.HasIndex("Expires");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.ToTable("ServerSideSessions", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Microser/Microser.IdS/Pages/Account/AccessDenied.cshtml
Normal file
10
src/Microser/Microser.IdS/Pages/Account/AccessDenied.cshtml
Normal file
@@ -0,0 +1,10 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Account.AccessDeniedModel
|
||||
@{
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Access Denied</h1>
|
||||
<p>You do not have permission to access that resource.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Account
|
||||
{
|
||||
public class AccessDeniedModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml
Normal file
40
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml
Normal file
@@ -0,0 +1,40 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Create.Index
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Create Account</h1>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-6">
|
||||
<form asp-page="/Account/Create/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Name"></label>
|
||||
<input type="text" class="form-control" placeholder="Name" asp-for="Input.Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input type="email" class="form-control" placeholder="Email" asp-for="Input.Email" >
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" name="Input.Button" value="create">Create</button>
|
||||
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
122
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml.cs
Normal file
122
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Test;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Create
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
TestUserStore? users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string? returnUrl)
|
||||
{
|
||||
Input = new InputModel { ReturnUrl = returnUrl };
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// check if we are in the context of an authorization request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
|
||||
// the user clicked the "cancel" button
|
||||
if (Input.Button != "create")
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
// if the user cancels, send a result back into IdentityServer as if they
|
||||
// denied the consent (even if this client does not require consent).
|
||||
// this will send back an access denied OIDC error response to the client.
|
||||
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// since we don't have a valid context, then we just go back to the home page
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
|
||||
if (_users.FindByUsername(Input.Username) != null)
|
||||
{
|
||||
ModelState.AddModelError("Input.Username", "Invalid username");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = _users.CreateUser(Input.Username, Input.Password, Input.Name, Input.Email);
|
||||
|
||||
// issue authentication cookie with subject ID and username
|
||||
var isuser = new IdentityServerUser(user.SubjectId)
|
||||
{
|
||||
DisplayName = user.Username
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(isuser);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we can trust Input.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microser/Microser.IdS/Pages/Account/Create/InputModel.cs
Normal file
23
src/Microser/Microser.IdS/Pages/Account/Create/InputModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Create
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
public string? Button { get; set; }
|
||||
}
|
||||
}
|
||||
89
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml
Normal file
89
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml
Normal file
@@ -0,0 +1,89 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Login.Index
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Login</h1>
|
||||
<p>Choose how to login</p>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
@if (Model.View.EnableLocalLogin)
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Local Account</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form asp-page="/Account/Login/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberLogin)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberLogin">
|
||||
<label class="form-check-label" asp-for="Input.RememberLogin">
|
||||
Remember My Login
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-primary" name="Input.Button" value="login">Login</button>
|
||||
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>External Account</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-inline">
|
||||
@foreach (var provider in Model.View.VisibleExternalProviders)
|
||||
{
|
||||
<li class="list-inline-item">
|
||||
<a class="btn btn-secondary"
|
||||
asp-page="/ExternalLogin/Challenge"
|
||||
asp-route-scheme="@provider.AuthenticationScheme"
|
||||
asp-route-returnUrl="@Model.Input.ReturnUrl">
|
||||
@provider.DisplayName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.View.EnableLocalLogin && !Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Invalid login request</strong>
|
||||
There are no login schemes configured for this request.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
231
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml.cs
Normal file
231
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Duende.IdentityServer.Test;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly IIdentityProviderStore _identityProviderStore;
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IIdentityProviderStore identityProviderStore,
|
||||
IEventService events,
|
||||
TestUserStore? users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
_schemeProvider = schemeProvider;
|
||||
_identityProviderStore = identityProviderStore;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? returnUrl)
|
||||
{
|
||||
await BuildModelAsync(returnUrl);
|
||||
|
||||
if (View.IsExternalLoginOnly)
|
||||
{
|
||||
// we only have one option for logging in and it's an external provider
|
||||
return RedirectToPage("/ExternalLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// check if we are in the context of an authorization request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
|
||||
// the user clicked the "cancel" button
|
||||
if (Input.Button != "login")
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
// if the user cancels, send a result back into IdentityServer as if they
|
||||
// denied the consent (even if this client does not require consent).
|
||||
// this will send back an access denied OIDC error response to the client.
|
||||
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// since we don't have a valid context, then we just go back to the home page
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// validate username/password against in-memory store
|
||||
if (_users.ValidateCredentials(Input.Username, Input.Password))
|
||||
{
|
||||
var user = _users.FindByUsername(Input.Username);
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);
|
||||
|
||||
// only set explicit expiration here if user chooses "remember me".
|
||||
// otherwise we rely upon expiration configured in cookie middleware.
|
||||
var props = new AuthenticationProperties();
|
||||
if (LoginOptions.AllowRememberLogin && Input.RememberLogin)
|
||||
{
|
||||
props.IsPersistent = true;
|
||||
props.ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration);
|
||||
};
|
||||
|
||||
// issue authentication cookie with subject ID and username
|
||||
var isuser = new IdentityServerUser(user.SubjectId)
|
||||
{
|
||||
DisplayName = user.Username
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(isuser, props);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
const string error = "invalid credentials";
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId: context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
|
||||
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
|
||||
// something went wrong, show form with error
|
||||
await BuildModelAsync(Input.ReturnUrl);
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task BuildModelAsync(string? returnUrl)
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
ReturnUrl = returnUrl
|
||||
};
|
||||
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
{
|
||||
var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider;
|
||||
|
||||
// this is meant to short circuit the UI and only trigger the one external IdP
|
||||
View = new ViewModel
|
||||
{
|
||||
EnableLocalLogin = local,
|
||||
};
|
||||
|
||||
Input.Username = context.LoginHint;
|
||||
|
||||
if (!local)
|
||||
{
|
||||
View.ExternalProviders = new[] { new ViewModel.ExternalProvider(authenticationScheme: context.IdP) };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var schemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
|
||||
var providers = schemes
|
||||
.Where(x => x.DisplayName != null)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Name,
|
||||
displayName: x.DisplayName ?? x.Name
|
||||
)).ToList();
|
||||
|
||||
var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())
|
||||
.Where(x => x.Enabled)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Scheme,
|
||||
displayName: x.DisplayName ?? x.Scheme
|
||||
));
|
||||
providers.AddRange(dynamicSchemes);
|
||||
|
||||
var allowLocal = true;
|
||||
var client = context?.Client;
|
||||
if (client != null)
|
||||
{
|
||||
allowLocal = client.EnableLocalLogin;
|
||||
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Count != 0)
|
||||
{
|
||||
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
View = new ViewModel
|
||||
{
|
||||
AllowRememberLogin = LoginOptions.AllowRememberLogin,
|
||||
EnableLocalLogin = allowLocal && LoginOptions.AllowLocalLogin,
|
||||
ExternalProviders = providers.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Microser/Microser.IdS/Pages/Account/Login/InputModel.cs
Normal file
20
src/Microser/Microser.IdS/Pages/Account/Login/InputModel.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
|
||||
public bool RememberLogin { get; set; }
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Button { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public static class LoginOptions
|
||||
{
|
||||
public static readonly bool AllowLocalLogin = true;
|
||||
public static readonly bool AllowRememberLogin = true;
|
||||
public static readonly TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
|
||||
public static readonly string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||
}
|
||||
}
|
||||
29
src/Microser/Microser.IdS/Pages/Account/Login/ViewModel.cs
Normal file
29
src/Microser/Microser.IdS/Pages/Account/Login/ViewModel.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public bool AllowRememberLogin { get; set; } = true;
|
||||
public bool EnableLocalLogin { get; set; } = true;
|
||||
|
||||
public IEnumerable<ViewModel.ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();
|
||||
public IEnumerable<ViewModel.ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
|
||||
|
||||
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
|
||||
public string? ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
|
||||
|
||||
public class ExternalProvider
|
||||
{
|
||||
public ExternalProvider(string authenticationScheme, string? displayName = null)
|
||||
{
|
||||
AuthenticationScheme = authenticationScheme;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
public string AuthenticationScheme { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml
Normal file
17
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml
Normal file
@@ -0,0 +1,17 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Logout.Index
|
||||
|
||||
<div class="logout-page">
|
||||
<div class="lead">
|
||||
<h1>Logout</h1>
|
||||
<p>Would you like to logout of IdentityServer?</p>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Account/Logout/Index">
|
||||
<input type="hidden" name="logoutId" value="@Model.LogoutId" />
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
101
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml.cs
Normal file
101
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
|
||||
[BindProperty]
|
||||
public string? LogoutId { get; set; }
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction, IEventService events)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? logoutId)
|
||||
{
|
||||
LogoutId = logoutId;
|
||||
|
||||
var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;
|
||||
|
||||
if (User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
// if the user is not authenticated, then just show logged out page
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var context = await _interaction.GetLogoutContextAsync(LogoutId);
|
||||
if (context?.ShowSignoutPrompt == false)
|
||||
{
|
||||
// it's safe to automatically sign-out
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (showLogoutPrompt == false)
|
||||
{
|
||||
// if the request for logout was properly authenticated from IdentityServer, then
|
||||
// we don't need to show the prompt and can just log the user out directly.
|
||||
return await OnPost();
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
// if there's no current logout context, we need to create one
|
||||
// this captures necessary info from the current logged in user
|
||||
// this can still return null if there is no context needed
|
||||
LogoutId ??= await _interaction.CreateLogoutContextAsync();
|
||||
|
||||
// delete local authentication cookie
|
||||
await HttpContext.SignOutAsync();
|
||||
|
||||
// see if we need to trigger federated logout
|
||||
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||
|
||||
// raise the logout event
|
||||
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
|
||||
Telemetry.Metrics.UserLogout(idp);
|
||||
|
||||
// if it's a local login we can ignore this workflow
|
||||
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
|
||||
{
|
||||
// we need to see if the provider supports external logout
|
||||
if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
|
||||
{
|
||||
// build a return URL so the upstream provider will redirect back
|
||||
// to us after the user has logged out. this allows us to then
|
||||
// complete our single sign-out processing.
|
||||
var url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
|
||||
|
||||
// this triggers a redirect to the external provider for sign-out
|
||||
return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Logout.LoggedOut
|
||||
|
||||
<div class="logged-out-page">
|
||||
<h1>
|
||||
Logout
|
||||
<small>You are now logged out</small>
|
||||
</h1>
|
||||
|
||||
@if (Model.View.PostLogoutRedirectUri != null)
|
||||
{
|
||||
<div>
|
||||
Click <a class="PostLogoutRedirectUri" href="@Model.View.PostLogoutRedirectUri">here</a> to return to the
|
||||
<span>@Model.View.ClientName</span> application.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.SignOutIframeUrl != null)
|
||||
{
|
||||
<iframe width="0" height="0" class="signout" src="@Model.View.SignOutIframeUrl"></iframe>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section scripts
|
||||
{
|
||||
@if (Model.View.AutomaticRedirectAfterSignOut)
|
||||
{
|
||||
<script src="~/js/signout-redirect.js"></script>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class LoggedOut : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interactionService;
|
||||
|
||||
public LoggedOutViewModel View { get; set; } = default!;
|
||||
|
||||
public LoggedOut(IIdentityServerInteractionService interactionService)
|
||||
{
|
||||
_interactionService = interactionService;
|
||||
}
|
||||
|
||||
public async Task OnGet(string? logoutId)
|
||||
{
|
||||
// get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
|
||||
|
||||
View = new LoggedOutViewModel
|
||||
{
|
||||
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
|
||||
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
|
||||
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
|
||||
SignOutIframeUrl = logout?.SignOutIFrameUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
public class LoggedOutViewModel
|
||||
{
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string? ClientName { get; set; }
|
||||
public string? SignOutIframeUrl { get; set; }
|
||||
public bool AutomaticRedirectAfterSignOut { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
public static class LogoutOptions
|
||||
{
|
||||
public static readonly bool ShowLogoutPrompt = true;
|
||||
public static readonly bool AutomaticRedirectAfterSignOut = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
public class ApiScopeSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class ApiScopeModel : ApiScopeSummaryModel
|
||||
{
|
||||
public string? UserClaims { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class ApiScopeRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public ApiScopeRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApiScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var query = _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.AsQueryable();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new ApiScopeSummaryModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<ApiScopeModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var scope = await _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) return null;
|
||||
|
||||
return new ApiScopeModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(ApiScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = new Duende.IdentityServer.Models.ApiScope()
|
||||
{
|
||||
Name = model.Name,
|
||||
DisplayName = model.DisplayName?.Trim()
|
||||
};
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
if (claims.Any())
|
||||
{
|
||||
scope.UserClaims = claims.ToList();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.ApiScopes.Add(scope.ToEntity());
|
||||
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ApiScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = await _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||
|
||||
if (scope.DisplayName != model.DisplayName)
|
||||
{
|
||||
scope.DisplayName = model.DisplayName?.Trim();
|
||||
}
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||
|
||||
if (claimsToRemove.Length != 0)
|
||||
{
|
||||
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||
}
|
||||
if (claimsToAdd.Length != 0)
|
||||
{
|
||||
scope.UserClaims.AddRange(claimsToAdd.Select(x => new ApiScopeClaim
|
||||
{
|
||||
Type = x,
|
||||
}));
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string id)
|
||||
{
|
||||
var scope = await _context.ApiScopes.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||
|
||||
_context.ApiScopes.Remove(scope);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Edit.cshtml
Normal file
39
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Edit.cshtml
Normal file
@@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="identityscopes-page">
|
||||
|
||||
<h2>
|
||||
Edit Identity Scope: @Model.InputModel.Name
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public EditModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ApiScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string Button { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/ApiScopes/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Index.cshtml
Normal file
54
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Index.cshtml
Normal file
@@ -0,0 +1,54 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
API Scopes
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/ApiScopes/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Display Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var scope in Model.Scopes)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/ApiScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||
<td>@scope.DisplayName</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public IndexModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<ApiScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Scopes = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/New.cshtml
Normal file
35
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/New.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<h2>
|
||||
New API Scope
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public NewModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ApiScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/ApiScopes/Edit", new { id = InputModel.Name });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
public class ClientSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string ClientId { get; set; } = default!;
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public Flow Flow { get; set; }
|
||||
}
|
||||
|
||||
public class CreateClientModel : ClientSummaryModel
|
||||
{
|
||||
public string Secret { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class ClientModel : CreateClientModel, IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public string AllowedScopes { get; set; } = default!;
|
||||
|
||||
public string? RedirectUri { get; set; }
|
||||
public string? InitiateLoginUri { get; set; }
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string? FrontChannelLogoutUri { get; set; }
|
||||
public string? BackChannelLogoutUri { get; set; }
|
||||
|
||||
private static readonly string[] memberNames = new[] { "RedirectUri" };
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
var errors = new List<ValidationResult>();
|
||||
|
||||
if (Flow == Flow.CodeFlowWithPkce)
|
||||
{
|
||||
if (RedirectUri == null)
|
||||
{
|
||||
errors.Add(new ValidationResult("Redirect URI is required.", memberNames));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Flow
|
||||
{
|
||||
ClientCredentials,
|
||||
CodeFlowWithPkce
|
||||
}
|
||||
|
||||
public class ClientRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public ClientRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ClientSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var grants = new[] { GrantType.AuthorizationCode, GrantType.ClientCredentials };
|
||||
|
||||
var query = _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Where(x => x.AllowedGrantTypes.Count == 1 && x.AllowedGrantTypes.Any(grant => grants.Contains(grant.GrantType)));
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.ClientId.Contains(filter) || x.ClientName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new ClientSummaryModel
|
||||
{
|
||||
ClientId = x.ClientId,
|
||||
Name = x.ClientName,
|
||||
Flow = x.AllowedGrantTypes.Select(x => x.GrantType).Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<ClientModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var client = await _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Include(x => x.AllowedScopes)
|
||||
.Include(x => x.RedirectUris)
|
||||
.Include(x => x.PostLogoutRedirectUris)
|
||||
.Where(x => x.ClientId == id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (client == null) return null;
|
||||
|
||||
return new ClientModel
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Name = client.ClientName,
|
||||
Flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce,
|
||||
AllowedScopes = client.AllowedScopes.Count != 0 ? client.AllowedScopes.Select(x => x.Scope).Aggregate((a, b) => $"{a} {b}") : string.Empty,
|
||||
RedirectUri = client.RedirectUris.Select(x => x.RedirectUri).SingleOrDefault(),
|
||||
InitiateLoginUri = client.InitiateLoginUri,
|
||||
PostLogoutRedirectUri = client.PostLogoutRedirectUris.Select(x => x.PostLogoutRedirectUri).SingleOrDefault(),
|
||||
FrontChannelLogoutUri = client.FrontChannelLogoutUri,
|
||||
BackChannelLogoutUri = client.BackChannelLogoutUri,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(CreateClientModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var client = new Duende.IdentityServer.Models.Client();
|
||||
client.ClientId = model.ClientId.Trim();
|
||||
client.ClientName = model.Name?.Trim();
|
||||
|
||||
client.ClientSecrets.Add(new Duende.IdentityServer.Models.Secret(model.Secret.Sha256()));
|
||||
|
||||
if (model.Flow == Flow.ClientCredentials)
|
||||
{
|
||||
client.AllowedGrantTypes = GrantTypes.ClientCredentials;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.AllowedGrantTypes = GrantTypes.Code;
|
||||
client.AllowOfflineAccess = true;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.Clients.Add(client.ToEntity());
|
||||
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ClientModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var client = await _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Include(x => x.AllowedScopes)
|
||||
.Include(x => x.RedirectUris)
|
||||
.Include(x => x.PostLogoutRedirectUris)
|
||||
.SingleOrDefaultAsync(x => x.ClientId == model.ClientId);
|
||||
|
||||
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||
|
||||
if (client.ClientName != model.Name)
|
||||
{
|
||||
client.ClientName = model.Name?.Trim();
|
||||
}
|
||||
|
||||
var scopes = model.AllowedScopes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||
var currentScopes = (client.AllowedScopes.Select(x => x.Scope) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var scopesToAdd = scopes.Except(currentScopes).ToArray();
|
||||
var scopesToRemove = currentScopes.Except(scopes).ToArray();
|
||||
|
||||
if (scopesToRemove.Length != 0)
|
||||
{
|
||||
client.AllowedScopes.RemoveAll(x => scopesToRemove.Contains(x.Scope));
|
||||
}
|
||||
if (scopesToAdd.Length != 0)
|
||||
{
|
||||
client.AllowedScopes.AddRange(scopesToAdd.Select(x => new ClientScope
|
||||
{
|
||||
Scope = x,
|
||||
}));
|
||||
}
|
||||
|
||||
var flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce;
|
||||
|
||||
if (flow == Flow.CodeFlowWithPkce)
|
||||
{
|
||||
if (client.RedirectUris.SingleOrDefault()?.RedirectUri != model.RedirectUri)
|
||||
{
|
||||
client.RedirectUris.Clear();
|
||||
if (model.RedirectUri != null)
|
||||
{
|
||||
client.RedirectUris.Add(new ClientRedirectUri { RedirectUri = model.RedirectUri.Trim() });
|
||||
}
|
||||
}
|
||||
if (client.InitiateLoginUri != model.InitiateLoginUri)
|
||||
{
|
||||
client.InitiateLoginUri = model.InitiateLoginUri;
|
||||
}
|
||||
if (client.PostLogoutRedirectUris.SingleOrDefault()?.PostLogoutRedirectUri != model.PostLogoutRedirectUri)
|
||||
{
|
||||
client.PostLogoutRedirectUris.Clear();
|
||||
if (model.PostLogoutRedirectUri != null)
|
||||
{
|
||||
client.PostLogoutRedirectUris.Add(new ClientPostLogoutRedirectUri { PostLogoutRedirectUri = model.PostLogoutRedirectUri.Trim() });
|
||||
}
|
||||
}
|
||||
if (client.FrontChannelLogoutUri != model.FrontChannelLogoutUri)
|
||||
{
|
||||
client.FrontChannelLogoutUri = model.FrontChannelLogoutUri?.Trim();
|
||||
}
|
||||
if (client.BackChannelLogoutUri != model.BackChannelLogoutUri)
|
||||
{
|
||||
client.BackChannelLogoutUri = model.BackChannelLogoutUri?.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string clientId)
|
||||
{
|
||||
var client = await _context.Clients.SingleOrDefaultAsync(x => x.ClientId == clientId);
|
||||
|
||||
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||
|
||||
_context.Clients.Remove(client);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml
Normal file
65
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml
Normal file
@@ -0,0 +1,65 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
|
||||
<h2>
|
||||
Edit Client Id: @Model.InputModel.ClientId
|
||||
(<small>@Model.InputModel.Flow</small>)
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.ClientId" />
|
||||
<input type="hidden" asp-for="@Model.InputModel.Flow" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.AllowedScopes">Allowed Scopes (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.AllowedScopes" />
|
||||
</div>
|
||||
|
||||
@if (Model.InputModel.Flow == Microser.IdS.Pages.Admin.Clients.Flow.CodeFlowWithPkce)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.RedirectUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.RedirectUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.InitiateLoginUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.InitiateLoginUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.PostLogoutRedirectUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.PostLogoutRedirectUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.FrontChannelLogoutUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.FrontChannelLogoutUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.BackChannelLogoutUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.BackChannelLogoutUri" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
58
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml.cs
Normal file
58
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public EditModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ClientModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string? Button { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/Clients/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/Clients/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/Clients/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Microser/Microser.IdS/Pages/Admin/Clients/Index.cshtml
Normal file
56
src/Microser/Microser.IdS/Pages/Admin/Clients/Index.cshtml
Normal file
@@ -0,0 +1,56 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
Clients
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/Clients/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Name</th>
|
||||
<th>Flow</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var client in Model.Clients)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/Clients/Edit" asp-route-id="@client.ClientId">@client.ClientId</a></td>
|
||||
<td>@client.Name</td>
|
||||
<td>@client.Flow</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public IndexModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<ClientSummaryModel> Clients { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Clients = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml
Normal file
56
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml
Normal file
@@ -0,0 +1,56 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
@if (Model.Created)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>Client Id <em>@Model.InputModel.ClientId</em> created</h2>
|
||||
<p>The client secret is displayed below. Copy it now, as it won't be shown again.</p>
|
||||
<p class="alert alert-danger">@Model.InputModel.Secret</p>
|
||||
<p>Click here to <a asp-page="/Admin/Clients/Edit" asp-route-id="@Model.InputModel.ClientId">continue</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2>
|
||||
New Client
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.ClientId"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.ClientId" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Secret"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Secret" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Flow"></label>
|
||||
<select class="form-control" asp-for="@Model.InputModel.Flow">
|
||||
<option value="0">Client Credentials</option>
|
||||
<option value="1">Code Flow (with PKCE)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
46
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml.cs
Normal file
46
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public NewModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public CreateClientModel InputModel { get; set; } = default!;
|
||||
|
||||
public bool Created { get; set; }
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
InputModel = new CreateClientModel
|
||||
{
|
||||
Secret = Convert.ToBase64String(CryptoRandom.CreateRandomKey(16))
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
Created = true;
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="identityscopes-page">
|
||||
|
||||
<h2>
|
||||
Edit Identity Scope: @Model.InputModel.Name
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public EditModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string? Button { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
public class IdentityScopeSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class IdentityScopeModel : IdentityScopeSummaryModel
|
||||
{
|
||||
public string? UserClaims { get; set; }
|
||||
}
|
||||
|
||||
public class IdentityScopeRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public IdentityScopeRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IdentityScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var query = _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.AsQueryable();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new IdentityScopeSummaryModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<IdentityScopeModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var scope = await _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) return null;
|
||||
|
||||
return new IdentityScopeModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(IdentityScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = new Duende.IdentityServer.Models.IdentityResource()
|
||||
{
|
||||
Name = model.Name,
|
||||
DisplayName = model.DisplayName?.Trim()
|
||||
};
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
if (claims.Any())
|
||||
{
|
||||
scope.UserClaims = claims.ToList();
|
||||
}
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.IdentityResources.Add(scope.ToEntity());
|
||||
#pragma warning restore CA1849
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(IdentityScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = await _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||
|
||||
if (scope.DisplayName != model.DisplayName)
|
||||
{
|
||||
scope.DisplayName = model.DisplayName?.Trim();
|
||||
}
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||
|
||||
if (claimsToRemove.Length != 0)
|
||||
{
|
||||
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||
}
|
||||
if (claimsToAdd.Length != 0)
|
||||
{
|
||||
scope.UserClaims.AddRange(claimsToAdd.Select(x => new IdentityResourceClaim
|
||||
{
|
||||
Type = x,
|
||||
}));
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string id)
|
||||
{
|
||||
var scope = await _context.IdentityResources.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||
|
||||
_context.IdentityResources.Remove(scope);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
Identity Scopes
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/IdentityScopes/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Display Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var scope in Model.Scopes)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/IdentityScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||
<td>@scope.DisplayName</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public IndexModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<IdentityScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Scopes = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<h2>
|
||||
New Identity Scope
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public NewModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id = InputModel.Name });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml
Normal file
23
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml
Normal file
@@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="admin-page">
|
||||
<h2>
|
||||
Duende IdentityServer Admin
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<p>There are simple administrative pages.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="list-group">
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/Clients/Index">Clients</a>
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/IdentityScopes/Index">Identity Scopes</a>
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/ApiScopes/Index">API Scopes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
17
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml.cs
Normal file
17
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml
Normal file
48
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml
Normal file
@@ -0,0 +1,48 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Ciba.AllModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-page">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Backchannel Login Requests</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.Logins.Any())
|
||||
{
|
||||
<table class="table table-bordered table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Client Id</th>
|
||||
<th>Binding Message</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var login in Model.Logins)
|
||||
{
|
||||
<tr>
|
||||
<td>@login.InternalId</td>
|
||||
<td>@login.Client.ClientId</td>
|
||||
<td>@login.BindingMessage</td>
|
||||
<td>
|
||||
<a asp-page="Consent" asp-route-id="@login.InternalId" class="btn btn-primary">Process</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>No Pending Login Requests</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
29
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml.cs
Normal file
29
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class AllModel : PageModel
|
||||
{
|
||||
public IEnumerable<BackchannelUserLoginRequest> Logins { get; set; } = default!;
|
||||
|
||||
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
|
||||
|
||||
public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService)
|
||||
{
|
||||
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
|
||||
}
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml
Normal file
98
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml
Normal file
@@ -0,0 +1,98 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Ciba.Consent
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-consent">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
|
||||
<h3>Verify that this identifier matches what the client is displaying: <em class="text-primary">@Model.View.BindingMessage</em></h3>
|
||||
|
||||
<p>Uncheck the permissions you do not wish to grant.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Ciba/Consent">
|
||||
<input type="hidden" asp-for="Input.Id" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.IdentityScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
Personal Information
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.ApiScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-tasks"></span>
|
||||
Application Access
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
229
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml.cs
Normal file
229
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class Consent : PageModel
|
||||
{
|
||||
private readonly IBackchannelAuthenticationInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<Consent> _logger;
|
||||
|
||||
public Consent(
|
||||
IBackchannelAuthenticationInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Consent> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? id)
|
||||
{
|
||||
if (!await SetViewModelAsync(id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(Input.Id ?? throw new ArgumentNullException(nameof(Input.Id)));
|
||||
if (request == null || request.Subject.GetSubjectId() != User.GetSubjectId())
|
||||
{
|
||||
_logger.InvalidId(Input.Id);
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
CompleteBackchannelLoginRequest? result = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id);
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id)
|
||||
{
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, result.ScopesValuesConsented, false);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.CompleteLoginRequestAsync(result);
|
||||
|
||||
return RedirectToPage("/Ciba/All");
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.Id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string? id)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(id);
|
||||
if (request != null && request.Subject.GetSubjectId() == User.GetSubjectId())
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoMatchingBackchannelLoginRequest(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(BackchannelUserLoginRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
||||
ClientUrl = request.Client.ClientUri,
|
||||
ClientLogoUrl = request.Client.LogoUri,
|
||||
BindingMessage = request.BindingMessage
|
||||
};
|
||||
|
||||
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources
|
||||
.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name)))
|
||||
.ToArray();
|
||||
|
||||
var resourceIndicators = request.RequestedResourceIndicators ?? Enumerable.Empty<string>();
|
||||
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
|
||||
|
||||
var apiScopes = new List<ScopeViewModel>();
|
||||
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
|
||||
{
|
||||
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
|
||||
if (apiScope != null)
|
||||
{
|
||||
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
|
||||
.Select(x => new ResourceViewModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName ?? x.Name,
|
||||
}).ToArray();
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
var displayName = apiScope.DisplayName ?? apiScope.Name;
|
||||
if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
|
||||
{
|
||||
displayName += ":" + parsedScopeValue.ParsedParameter;
|
||||
}
|
||||
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = parsedScopeValue.ParsedName,
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Microser/Microser.IdS/Pages/Ciba/ConsentOptions.cs
Normal file
15
src/Microser/Microser.IdS/Pages/Ciba/ConsentOptions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
public static class ConsentOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
|
||||
|
||||
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
|
||||
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
|
||||
}
|
||||
}
|
||||
30
src/Microser/Microser.IdS/Pages/Ciba/Index.cshtml
Normal file
30
src/Microser/Microser.IdS/Pages/Ciba/Index.cshtml
Normal file
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Ciba.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-page">
|
||||
<div class="lead">
|
||||
@if (Model.LoginRequest.Client.LogoUri != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.LoginRequest.Client.LogoUri"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.LoginRequest.Client.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
|
||||
<h3>
|
||||
Verify that this identifier matches what the client is displaying:
|
||||
<em class="text-primary">@Model.LoginRequest.BindingMessage</em>
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
Do you wish to continue?
|
||||
</p>
|
||||
<div>
|
||||
<a class="btn btn-primary" asp-page="/Ciba/Consent" asp-route-id="@Model.LoginRequest.InternalId">Yes, Continue</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
43
src/Microser/Microser.IdS/Pages/Ciba/Index.cshtml.cs
Normal file
43
src/Microser/Microser.IdS/Pages/Ciba/Index.cshtml.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public BackchannelUserLoginRequest LoginRequest { get; set; } = default!;
|
||||
|
||||
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
|
||||
private readonly ILogger<IndexModel> _logger;
|
||||
|
||||
public IndexModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService, ILogger<IndexModel> logger)
|
||||
{
|
||||
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string id)
|
||||
{
|
||||
var result = await _backchannelAuthenticationInteraction.GetLoginRequestByInternalIdAsync(id);
|
||||
if (result == null)
|
||||
{
|
||||
_logger.InvalidBackchannelLoginId(id);
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoginRequest = result;
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Microser/Microser.IdS/Pages/Ciba/InputModel.cs
Normal file
13
src/Microser/Microser.IdS/Pages/Ciba/InputModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public string? Id { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Ciba/ViewModel.cs
Normal file
35
src/Microser/Microser.IdS/Pages/Ciba/ViewModel.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public string? ClientName { get; set; }
|
||||
public string? ClientUrl { get; set; }
|
||||
public string? ClientLogoUrl { get; set; }
|
||||
|
||||
public string? BindingMessage { get; set; }
|
||||
|
||||
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Emphasize { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Checked { get; set; }
|
||||
public IEnumerable<ResourceViewModel> Resources { get; set; } = Enumerable.Empty<ResourceViewModel>();
|
||||
}
|
||||
|
||||
public class ResourceViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
}
|
||||
47
src/Microser/Microser.IdS/Pages/Ciba/_ScopeListItem.cshtml
Normal file
47
src/Microser/Microser.IdS/Pages/Ciba/_ScopeListItem.cshtml
Normal file
@@ -0,0 +1,47 @@
|
||||
@using Microser.IdS.Pages.Ciba
|
||||
@model ScopeViewModel
|
||||
|
||||
<li class="list-group-item">
|
||||
<label>
|
||||
<input class="consent-scopecheck"
|
||||
type="checkbox"
|
||||
name="Input.ScopesConsented"
|
||||
id="scopes_@Model.Value"
|
||||
value="@Model.Value"
|
||||
checked="@Model.Checked"
|
||||
disabled="@Model.Required" />
|
||||
@if (Model.Required)
|
||||
{
|
||||
<input type="hidden"
|
||||
name="Input.ScopesConsented"
|
||||
value="@Model.Value" />
|
||||
}
|
||||
<strong>@Model.DisplayName</strong>
|
||||
@if (Model.Emphasize)
|
||||
{
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
}
|
||||
</label>
|
||||
@if (Model.Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
@if (Model.Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label for="scopes_@Model.Value">@Model.Description</label>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Resources?.Any() == true)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label>Will be available to these resource servers:</label>
|
||||
<ul>
|
||||
@foreach (var resource in Model.Resources)
|
||||
{
|
||||
<li>@resource.DisplayName</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
15
src/Microser/Microser.IdS/Pages/Consent/ConsentOptions.cs
Normal file
15
src/Microser/Microser.IdS/Pages/Consent/ConsentOptions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Consent
|
||||
{
|
||||
public static class ConsentOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
|
||||
|
||||
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
|
||||
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
|
||||
}
|
||||
}
|
||||
107
src/Microser/Microser.IdS/Pages/Consent/Index.cshtml
Normal file
107
src/Microser/Microser.IdS/Pages/Consent/Index.cshtml
Normal file
@@ -0,0 +1,107 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Consent.Index
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="page-consent">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
<p>Uncheck the permissions you do not wish to grant.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Consent/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.IdentityScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
Personal Information
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.ApiScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-tasks"></span>
|
||||
Application Access
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberConsent)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberConsent">
|
||||
<label class="form-check-label" asp-for="Input.RememberConsent">
|
||||
<strong>Remember My Decision</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
237
src/Microser/Microser.IdS/Pages/Consent/Index.cshtml.cs
Normal file
237
src/Microser/Microser.IdS/Pages/Consent/Index.cshtml.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Consent
|
||||
{
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<Index> _logger;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Index> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? returnUrl)
|
||||
{
|
||||
if (!await SetViewModelAsync(returnUrl))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
ReturnUrl = returnUrl,
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
if (request == null) return RedirectToPage("/Home/Error/Index");
|
||||
|
||||
ConsentResponse? grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = Input.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.GrantConsentAsync(request, grantedConsent);
|
||||
|
||||
// redirect back to authorization endpoint
|
||||
if (request.IsNativeClient() == true)
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.ReturnUrl))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string? returnUrl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(returnUrl);
|
||||
|
||||
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (request != null)
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoConsentMatchingRequest(returnUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(AuthorizationRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
||||
ClientUrl = request.Client.ClientUri,
|
||||
ClientLogoUrl = request.Client.LogoUri,
|
||||
AllowRememberConsent = request.Client.AllowRememberConsent
|
||||
};
|
||||
|
||||
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources
|
||||
.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name)))
|
||||
.ToArray();
|
||||
|
||||
var resourceIndicators = request.Parameters.GetValues(OidcConstants.AuthorizeRequest.Resource) ?? Enumerable.Empty<string>();
|
||||
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
|
||||
|
||||
var apiScopes = new List<ScopeViewModel>();
|
||||
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
|
||||
{
|
||||
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
|
||||
if (apiScope != null)
|
||||
{
|
||||
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
|
||||
.Select(x => new ResourceViewModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName ?? x.Name,
|
||||
}).ToArray();
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(CreateOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
var displayName = apiScope.DisplayName ?? apiScope.Name;
|
||||
if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
|
||||
{
|
||||
displayName += ":" + parsedScopeValue.ParsedParameter;
|
||||
}
|
||||
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = parsedScopeValue.ParsedName,
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Microser/Microser.IdS/Pages/Consent/InputModel.cs
Normal file
14
src/Microser/Microser.IdS/Pages/Consent/InputModel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Consent
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public bool RememberConsent { get; set; } = true;
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
}
|
||||
34
src/Microser/Microser.IdS/Pages/Consent/ViewModel.cs
Normal file
34
src/Microser/Microser.IdS/Pages/Consent/ViewModel.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Consent
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public string? ClientName { get; set; }
|
||||
public string? ClientUrl { get; set; }
|
||||
public string? ClientLogoUrl { get; set; }
|
||||
public bool AllowRememberConsent { get; set; }
|
||||
|
||||
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Value { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Emphasize { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Checked { get; set; }
|
||||
public IEnumerable<ResourceViewModel> Resources { get; set; } = Enumerable.Empty<ResourceViewModel>();
|
||||
}
|
||||
|
||||
public class ResourceViewModel
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@using Microser.IdS.Pages.Consent
|
||||
@model ScopeViewModel
|
||||
|
||||
<li class="list-group-item">
|
||||
<label>
|
||||
<input class="consent-scopecheck"
|
||||
type="checkbox"
|
||||
name="Input.ScopesConsented"
|
||||
id="scopes_@Model.Value"
|
||||
value="@Model.Value"
|
||||
checked="@Model.Checked"
|
||||
disabled="@Model.Required" />
|
||||
@if (Model.Required)
|
||||
{
|
||||
<input type="hidden"
|
||||
name="Input.ScopesConsented"
|
||||
value="@Model.Value" />
|
||||
}
|
||||
<strong>@Model.DisplayName</strong>
|
||||
@if (Model.Emphasize)
|
||||
{
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
}
|
||||
</label>
|
||||
@if (Model.Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
@if (Model.Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label for="scopes_@Model.Value">@Model.Description</label>
|
||||
</div>
|
||||
}
|
||||
@if (Model.Resources?.Any() == true)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label>Will be available to these resource servers:</label>
|
||||
<ul>
|
||||
@foreach (var resource in Model.Resources)
|
||||
{
|
||||
<li>@resource.DisplayName</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
16
src/Microser/Microser.IdS/Pages/Device/DeviceOptions.cs
Normal file
16
src/Microser/Microser.IdS/Pages/Device/DeviceOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
public static class DeviceOptions
|
||||
{
|
||||
public static readonly bool EnableOfflineAccess = true;
|
||||
public static readonly string OfflineAccessDisplayName = "Offline Access";
|
||||
public static readonly string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
|
||||
|
||||
public static readonly string InvalidUserCode = "Invalid user code";
|
||||
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
|
||||
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
|
||||
}
|
||||
}
|
||||
141
src/Microser/Microser.IdS/Pages/Device/Index.cshtml
Normal file
141
src/Microser/Microser.IdS/Pages/Device/Index.cshtml
Normal file
@@ -0,0 +1,141 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Device.Index
|
||||
@{
|
||||
}
|
||||
|
||||
@if (Model.Input.UserCode == null)
|
||||
{
|
||||
@*We need to collect the user code*@
|
||||
<div class="page-device-code">
|
||||
<div class="lead">
|
||||
<h1>User Code</h1>
|
||||
<p>Please enter the code displayed on your device.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<form asp-page="/Device/Index" method="get">
|
||||
<div class="form-group">
|
||||
<label for="userCode">User Code:</label>
|
||||
<input class="form-control" for="userCode" name="userCode" autofocus />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" name="button">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@*collect consent for the user code provided*@
|
||||
<div class="page-device-confirmation">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
<p>Please confirm that the authorization request matches the code: <strong>@Model.Input.UserCode</strong>.</p>
|
||||
<p>Uncheck the permissions you do not wish to grant.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Device/Index">
|
||||
<input asp-for="Input.UserCode" type="hidden" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.IdentityScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
Personal Information
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.ApiScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-tasks"></span>
|
||||
Application Access
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberConsent)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberConsent">
|
||||
<label class="form-check-label" asp-for="Input.RememberConsent">
|
||||
<strong>Remember My Decision</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
221
src/Microser/Microser.IdS/Pages/Device/Index.cshtml.cs
Normal file
221
src/Microser/Microser.IdS/Pages/Device/Index.cshtml.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Configuration;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microser.IdS.Pages.Consent;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IDeviceFlowInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly IOptions<IdentityServerOptions> _options;
|
||||
private readonly ILogger<Index> _logger;
|
||||
|
||||
public Index(
|
||||
IDeviceFlowInteractionService interaction,
|
||||
IEventService eventService,
|
||||
IOptions<IdentityServerOptions> options,
|
||||
ILogger<Index> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = eventService;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? userCode)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(userCode))
|
||||
{
|
||||
return Page();
|
||||
}
|
||||
|
||||
if (!await SetViewModelAsync(userCode))
|
||||
{
|
||||
ModelState.AddModelError("", DeviceOptions.InvalidUserCode);
|
||||
return Page();
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
UserCode = userCode,
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(Input.UserCode ?? throw new ArgumentNullException(nameof(Input.UserCode)));
|
||||
if (request == null) return RedirectToPage("/Home/Error/Index");
|
||||
|
||||
ConsentResponse? grantedConsent = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
Error = AuthorizationError.AccessDenied
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
grantedConsent = new ConsentResponse
|
||||
{
|
||||
RememberConsent = Input.RememberConsent,
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (grantedConsent != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.HandleRequestAsync(Input.UserCode, grantedConsent);
|
||||
|
||||
// indicate that's it ok to redirect back to authorization endpoint
|
||||
return RedirectToPage("/Device/Success");
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.UserCode))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string userCode)
|
||||
{
|
||||
var request = await _interaction.GetAuthorizationContextAsync(userCode);
|
||||
if (request != null)
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
View = new ViewModel();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(DeviceFlowAuthorizationRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
||||
ClientUrl = request.Client.ClientUri,
|
||||
ClientLogoUrl = request.Client.LogoUri,
|
||||
AllowRememberConsent = request.Client.AllowRememberConsent
|
||||
};
|
||||
|
||||
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name))).ToArray();
|
||||
|
||||
var apiScopes = new List<ScopeViewModel>();
|
||||
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
|
||||
{
|
||||
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
|
||||
if (apiScope != null)
|
||||
{
|
||||
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (DeviceOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = parsedScopeValue.RawValue,
|
||||
// todo: use the parsed scope value in the display?
|
||||
DisplayName = apiScope.DisplayName ?? apiScope.Name,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = DeviceOptions.OfflineAccessDisplayName,
|
||||
Description = DeviceOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Microser/Microser.IdS/Pages/Device/InputModel.cs
Normal file
15
src/Microser/Microser.IdS/Pages/Device/InputModel.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
public string? Button { get; set; }
|
||||
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
|
||||
public bool RememberConsent { get; set; } = true;
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? UserCode { get; set; }
|
||||
}
|
||||
}
|
||||
12
src/Microser/Microser.IdS/Pages/Device/Success.cshtml
Normal file
12
src/Microser/Microser.IdS/Pages/Device/Success.cshtml
Normal file
@@ -0,0 +1,12 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Device.SuccessModel
|
||||
@{
|
||||
}
|
||||
|
||||
|
||||
<div class="page-device-success">
|
||||
<div class="lead">
|
||||
<h1>Success</h1>
|
||||
<p>You have successfully authorized the device</p>
|
||||
</div>
|
||||
</div>
|
||||
17
src/Microser/Microser.IdS/Pages/Device/Success.cshtml.cs
Normal file
17
src/Microser/Microser.IdS/Pages/Device/Success.cshtml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class SuccessModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Microser/Microser.IdS/Pages/Device/ViewModel.cs
Normal file
26
src/Microser/Microser.IdS/Pages/Device/ViewModel.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public string? ClientName { get; set; }
|
||||
public string? ClientUrl { get; set; }
|
||||
public string? ClientLogoUrl { get; set; }
|
||||
public bool AllowRememberConsent { get; set; }
|
||||
|
||||
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
|
||||
}
|
||||
|
||||
public class ScopeViewModel
|
||||
{
|
||||
public string? Value { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Emphasize { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Checked { get; set; }
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Device/_ScopeListItem.cshtml
Normal file
35
src/Microser/Microser.IdS/Pages/Device/_ScopeListItem.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@using Microser.IdS.Pages.Device
|
||||
@model ScopeViewModel
|
||||
|
||||
<li class="list-group-item">
|
||||
<label>
|
||||
<input class="consent-scopecheck"
|
||||
type="checkbox"
|
||||
name="Input.ScopesConsented"
|
||||
id="scopes_@Model.Value"
|
||||
value="@Model.Value"
|
||||
checked="@Model.Checked"
|
||||
disabled="@Model.Required" />
|
||||
@if (Model.Required)
|
||||
{
|
||||
<input type="hidden"
|
||||
name="Input.ScopesConsented"
|
||||
value="@Model.Value" />
|
||||
}
|
||||
<strong>@Model.DisplayName</strong>
|
||||
@if (Model.Emphasize)
|
||||
{
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
}
|
||||
</label>
|
||||
@if (Model.Required)
|
||||
{
|
||||
<span><em>(required)</em></span>
|
||||
}
|
||||
@if (Model.Description != null)
|
||||
{
|
||||
<div class="consent-description">
|
||||
<label for="scopes_@Model.Value">@Model.Description</label>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
67
src/Microser/Microser.IdS/Pages/Diagnostics/Index.cshtml
Normal file
67
src/Microser/Microser.IdS/Pages/Diagnostics/Index.cshtml
Normal file
@@ -0,0 +1,67 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Diagnostics.Index
|
||||
|
||||
<div class="diagnostics-page">
|
||||
<div class="lead">
|
||||
<h1>Authentication Cookie</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Claims</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(Model.View.AuthenticateResult.Principal != null)
|
||||
{
|
||||
<dl>
|
||||
@foreach (var claim in Model.View.AuthenticateResult.Principal.Claims)
|
||||
{
|
||||
<dt>@claim.Type</dt>
|
||||
<dd>@claim.Value</dd>
|
||||
}
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Properties</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl>
|
||||
@if (Model.View.AuthenticateResult.Properties != null)
|
||||
{
|
||||
@foreach (var prop in Model.View.AuthenticateResult.Properties.Items)
|
||||
{
|
||||
<dt>@prop.Key</dt>
|
||||
<dd>@prop.Value</dd>
|
||||
}
|
||||
}
|
||||
@if (Model.View.Clients.Any())
|
||||
{
|
||||
<dt>Clients</dt>
|
||||
<dd>
|
||||
@{
|
||||
var clients = Model.View.Clients.ToArray();
|
||||
for(var i = 0; i < clients.Length; i++)
|
||||
{
|
||||
<text>@clients[i]</text>
|
||||
if (i < clients.Length - 1)
|
||||
{
|
||||
<text>, </text>
|
||||
}
|
||||
}
|
||||
}
|
||||
</dd>
|
||||
}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
35
src/Microser/Microser.IdS/Pages/Diagnostics/Index.cshtml.cs
Normal file
35
src/Microser/Microser.IdS/Pages/Diagnostics/Index.cshtml.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Diagnostics
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
var localAddresses = new List<string?> { "127.0.0.1", "::1" };
|
||||
if (HttpContext.Connection.LocalIpAddress != null)
|
||||
{
|
||||
localAddresses.Add(HttpContext.Connection.LocalIpAddress.ToString());
|
||||
}
|
||||
|
||||
if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress?.ToString()))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
View = new ViewModel(await HttpContext.AuthenticateAsync());
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Microser/Microser.IdS/Pages/Diagnostics/ViewModel.cs
Normal file
33
src/Microser/Microser.IdS/Pages/Diagnostics/ViewModel.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microser.IdS.Pages.Diagnostics
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public ViewModel(AuthenticateResult result)
|
||||
{
|
||||
AuthenticateResult = result;
|
||||
|
||||
if (result?.Properties?.Items.TryGetValue("client_list", out var encoded) == true)
|
||||
{
|
||||
if (encoded != null)
|
||||
{
|
||||
var bytes = Base64Url.Decode(encoded);
|
||||
var value = Encoding.UTF8.GetString(bytes);
|
||||
Clients = JsonSerializer.Deserialize<string[]>(value) ?? Enumerable.Empty<string>();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Clients = Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
public AuthenticateResult AuthenticateResult { get; }
|
||||
public IEnumerable<string> Clients { get; }
|
||||
}
|
||||
}
|
||||
43
src/Microser/Microser.IdS/Pages/Extensions.cs
Normal file
43
src/Microser/Microser.IdS/Pages/Extensions.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if the authentication scheme support signout.
|
||||
/// </summary>
|
||||
internal static async Task<bool> GetSchemeSupportsSignOutAsync(this HttpContext context, string scheme)
|
||||
{
|
||||
var provider = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
|
||||
var handler = await provider.GetHandlerAsync(context, scheme);
|
||||
return (handler is IAuthenticationSignOutHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the redirect URI is for a native client.
|
||||
/// </summary>
|
||||
internal static bool IsNativeClient(this AuthorizationRequest context)
|
||||
{
|
||||
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a loading page that is used to redirect back to the redirectUri.
|
||||
/// </summary>
|
||||
internal static IActionResult LoadingPage(this PageModel page, string? redirectUri)
|
||||
{
|
||||
page.HttpContext.Response.StatusCode = 200;
|
||||
page.HttpContext.Response.Headers["Location"] = "";
|
||||
|
||||
return page.RedirectToPage("/Redirect/Index", new { RedirectUri = redirectUri });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.ExternalLogin.Callback
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
149
src/Microser/Microser.IdS/Pages/ExternalLogin/Callback.cshtml.cs
Normal file
149
src/Microser/Microser.IdS/Pages/ExternalLogin/Callback.cshtml.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Test;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Microser.IdS.Pages.ExternalLogin
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Callback : PageModel
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly ILogger<Callback> _logger;
|
||||
private readonly IEventService _events;
|
||||
|
||||
public Callback(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Callback> logger,
|
||||
TestUserStore? users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
_logger = logger;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
{
|
||||
// read external identity from the temporary cookie
|
||||
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
if (result.Succeeded != true)
|
||||
{
|
||||
throw new InvalidOperationException($"External authentication error: {result.Failure}");
|
||||
}
|
||||
|
||||
var externalUser = result.Principal ??
|
||||
throw new InvalidOperationException("External authentication produced a null Principal");
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var externalClaims = externalUser.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||
_logger.ExternalClaims(externalClaims);
|
||||
}
|
||||
|
||||
// lookup our user and external provider info
|
||||
// try to determine the unique id of the external user (issued by the provider)
|
||||
// the most common claim type for that are the sub claim and the NameIdentifier
|
||||
// depending on the external provider, some other claim type might be used
|
||||
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
|
||||
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
|
||||
throw new InvalidOperationException("Unknown userid");
|
||||
|
||||
var provider = result.Properties.Items["scheme"] ?? throw new InvalidOperationException("Null scheme in authentiation properties");
|
||||
var providerUserId = userIdClaim.Value;
|
||||
|
||||
// find external user
|
||||
var user = _users.FindByExternalProvider(provider, providerUserId);
|
||||
if (user == null)
|
||||
{
|
||||
// this might be where you might initiate a custom workflow for user registration
|
||||
// in this sample we don't show how that would be done, as our sample implementation
|
||||
// simply auto-provisions new external user
|
||||
//
|
||||
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
|
||||
var claims = externalUser.Claims.ToList();
|
||||
claims.Remove(userIdClaim);
|
||||
user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
|
||||
}
|
||||
|
||||
// this allows us to collect any additional claims or properties
|
||||
// for the specific protocols used and store them in the local auth cookie.
|
||||
// this is typically used to store data needed for signout from those protocols.
|
||||
var additionalLocalClaims = new List<Claim>();
|
||||
var localSignInProps = new AuthenticationProperties();
|
||||
CaptureExternalLoginContext(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// issue authentication cookie for user
|
||||
var isuser = new IdentityServerUser(user.SubjectId)
|
||||
{
|
||||
DisplayName = user.Username,
|
||||
IdentityProvider = provider,
|
||||
AdditionalClaims = additionalLocalClaims
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(isuser, localSignInProps);
|
||||
|
||||
// delete temporary cookie used during external authentication
|
||||
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
|
||||
// retrieve return URL
|
||||
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
|
||||
|
||||
// check if external login is in the context of an OIDC request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLogin(context?.Client.ClientId, provider!);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(returnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
// if the external login is OIDC-based, there are certain things we need to preserve to make logout work
|
||||
// this will be different for WS-Fed, SAML2p or other protocols
|
||||
private static void CaptureExternalLoginContext(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(externalResult.Principal, nameof(externalResult.Principal));
|
||||
|
||||
// capture the idp used to login, so the session knows where the user came from
|
||||
localClaims.Add(new Claim(JwtClaimTypes.IdentityProvider, externalResult.Properties?.Items["scheme"] ?? "unknown identity provider"));
|
||||
|
||||
// if the external system sent a session id claim, copy it over
|
||||
// so we can use it for single sign-out
|
||||
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
|
||||
if (sid != null)
|
||||
{
|
||||
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
|
||||
}
|
||||
|
||||
// if the external provider issued an id_token, we'll keep it for signout
|
||||
var idToken = externalResult.Properties?.GetTokenValue("id_token");
|
||||
if (idToken != null)
|
||||
{
|
||||
localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.ExternalLogin.Challenge
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.ExternalLogin
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Challenge : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interactionService;
|
||||
|
||||
public Challenge(IIdentityServerInteractionService interactionService)
|
||||
{
|
||||
_interactionService = interactionService;
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string scheme, string? returnUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
|
||||
|
||||
// validate returnUrl - either it is a valid OIDC URL or back to a local page
|
||||
if (Url.IsLocalUrl(returnUrl) == false && _interactionService.IsValidReturnUrl(returnUrl) == false)
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
|
||||
// start challenge and roundtrip the return URL and scheme
|
||||
var props = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Url.Page("/externallogin/callback"),
|
||||
|
||||
Items =
|
||||
{
|
||||
{ "returnUrl", returnUrl },
|
||||
{ "scheme", scheme },
|
||||
}
|
||||
};
|
||||
|
||||
return Challenge(props, scheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Microser/Microser.IdS/Pages/Grants/Index.cshtml
Normal file
90
src/Microser/Microser.IdS/Pages/Grants/Index.cshtml
Normal file
@@ -0,0 +1,90 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Grants.Index
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="grants-page">
|
||||
<div class="lead">
|
||||
<h1>Client Application Permissions</h1>
|
||||
<p>Below is the list of applications you have given permission to and the resources they have access to.</p>
|
||||
</div>
|
||||
|
||||
@if (!Model.View.Grants.Any())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="alert alert-info">
|
||||
You have not given access to any applications
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var grant in Model.View.Grants)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-sm-8 card-title">
|
||||
@if (grant.ClientLogoUrl != null)
|
||||
{
|
||||
<img src="@grant.ClientLogoUrl">
|
||||
}
|
||||
<strong>@grant.ClientName</strong>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<form asp-page="/Grants/Index">
|
||||
<input type="hidden" name="clientId" value="@grant.ClientId">
|
||||
<button class="btn btn-danger">Revoke Access</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
@if (grant.Description != null)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<label>Description:</label> @grant.Description
|
||||
</li>
|
||||
}
|
||||
<li class="list-group-item">
|
||||
<label>Created:</label> @grant.Created.ToString("yyyy-MM-dd")
|
||||
</li>
|
||||
@if (grant.Expires.HasValue)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<label>Expires:</label> @grant.Expires.Value.ToString("yyyy-MM-dd")
|
||||
</li>
|
||||
}
|
||||
@if (grant.IdentityGrantNames.Any())
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<label>Identity Grants</label>
|
||||
<ul>
|
||||
@foreach (var name in grant.IdentityGrantNames)
|
||||
{
|
||||
<li>@name</li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
@if (grant.ApiGrantNames.Any())
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<label>API Grants</label>
|
||||
<ul>
|
||||
@foreach (var name in grant.ApiGrantNames)
|
||||
{
|
||||
<li>@name</li>
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
83
src/Microser/Microser.IdS/Pages/Grants/Index.cshtml.cs
Normal file
83
src/Microser/Microser.IdS/Pages/Grants/Index.cshtml.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Grants
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IClientStore _clients;
|
||||
private readonly IResourceStore _resources;
|
||||
private readonly IEventService _events;
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction,
|
||||
IClientStore clients,
|
||||
IResourceStore resources,
|
||||
IEventService events)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_clients = clients;
|
||||
_resources = resources;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
var grants = await _interaction.GetAllUserGrantsAsync();
|
||||
|
||||
var list = new List<GrantViewModel>();
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
var client = await _clients.FindClientByIdAsync(grant.ClientId);
|
||||
if (client != null)
|
||||
{
|
||||
var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes);
|
||||
|
||||
var item = new GrantViewModel()
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
ClientName = client.ClientName ?? client.ClientId,
|
||||
ClientLogoUrl = client.LogoUri,
|
||||
ClientUrl = client.ClientUri,
|
||||
Description = grant.Description,
|
||||
Created = grant.CreationTime,
|
||||
Expires = grant.Expiration,
|
||||
IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),
|
||||
ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray()
|
||||
};
|
||||
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
View = new ViewModel
|
||||
{
|
||||
Grants = list
|
||||
};
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
await _interaction.RevokeUserConsentAsync(ClientId);
|
||||
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), ClientId));
|
||||
Telemetry.Metrics.GrantsRevoked(ClientId);
|
||||
|
||||
return RedirectToPage("/Grants/Index");
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microser/Microser.IdS/Pages/Grants/ViewModel.cs
Normal file
23
src/Microser/Microser.IdS/Pages/Grants/ViewModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Grants
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public IEnumerable<GrantViewModel> Grants { get; set; } = Enumerable.Empty<GrantViewModel>();
|
||||
}
|
||||
|
||||
public class GrantViewModel
|
||||
{
|
||||
public string? ClientId { get; set; }
|
||||
public string? ClientName { get; set; }
|
||||
public string? ClientUrl { get; set; }
|
||||
public string? ClientLogoUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? Expires { get; set; }
|
||||
public IEnumerable<string> IdentityGrantNames { get; set; } = Enumerable.Empty<string>();
|
||||
public IEnumerable<string> ApiGrantNames { get; set; } = Enumerable.Empty<string>();
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Home/Error/Index.cshtml
Normal file
35
src/Microser/Microser.IdS/Pages/Home/Error/Index.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Error.Index
|
||||
|
||||
<div class="error-page">
|
||||
<div class="lead">
|
||||
<h1>Error</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="alert alert-danger">
|
||||
Sorry, there was an error
|
||||
|
||||
@if (Model.View.Error != null)
|
||||
{
|
||||
<strong>
|
||||
<em>
|
||||
: @Model.View.Error.Error
|
||||
</em>
|
||||
</strong>
|
||||
|
||||
if (Model.View.Error.ErrorDescription != null)
|
||||
{
|
||||
<div>@Model.View.Error.ErrorDescription</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model?.View?.Error?.RequestId != null)
|
||||
{
|
||||
<div class="request-id">Request Id: @Model.View.Error.RequestId</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
41
src/Microser/Microser.IdS/Pages/Home/Error/Index.cshtml.cs
Normal file
41
src/Microser/Microser.IdS/Pages/Home/Error/Index.cshtml.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Error
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[SecurityHeaders]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public ViewModel View { get; set; } = new();
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction, IWebHostEnvironment environment)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task OnGet(string? errorId)
|
||||
{
|
||||
// retrieve error details from identityserver
|
||||
var message = await _interaction.GetErrorContextAsync(errorId);
|
||||
if (message != null)
|
||||
{
|
||||
View.Error = message;
|
||||
|
||||
if (!_environment.IsDevelopment())
|
||||
{
|
||||
// only show in development
|
||||
message.ErrorDescription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Microser/Microser.IdS/Pages/Home/Error/ViewModel.cs
Normal file
21
src/Microser/Microser.IdS/Pages/Home/Error/ViewModel.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Microser.IdS.Pages.Error
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public ViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
public ViewModel(string error)
|
||||
{
|
||||
Error = new ErrorMessage { Error = error };
|
||||
}
|
||||
|
||||
public ErrorMessage? Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
// global/shared
|
||||
[assembly: SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Consistent with the IdentityServer APIs")]
|
||||
[assembly: SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "Consistent with the IdentityServer APIs")]
|
||||
[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "No need for ConfigureAwait in ASP.NET Core application code, as there is no SynchronizationContext.")]
|
||||
|
||||
// page specific
|
||||
[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "TestUsers are not designed to be extended", Scope = "member", Target = "~P:Microser.IdS.TestUsers.Users")]
|
||||
[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "ExternalProvider is nested by design", Scope = "type", Target = "~T:Microser.IdS.Pages.Login.ViewModel.ExternalProvider")]
|
||||
[assembly: SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "This namespace is just for organization, and won't be referenced elsewhere", Scope = "namespace", Target = "~N:Microser.IdS.Pages.Error")]
|
||||
[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Namespaces of pages are not likely to be used elsewhere, so there is little chance of confusion", Scope = "type", Target = "~T:Microser.IdS.Pages.Ciba.Consent")]
|
||||
[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Namespaces of pages are not likely to be used elsewhere, so there is little chance of confusion", Scope = "type", Target = "~T:Microser.IdS.Pages.Extensions")]
|
||||
[assembly: SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "This is for clarity and consistency with the surrounding code", Scope = "member", Target = "~F:Microser.IdS.Pages.Logout.LogoutOptions.AutomaticRedirectAfterSignOut")]
|
||||
52
src/Microser/Microser.IdS/Pages/Index.cshtml
Normal file
52
src/Microser/Microser.IdS/Pages/Index.cshtml
Normal file
@@ -0,0 +1,52 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Home.Index
|
||||
|
||||
<div class="welcome-page">
|
||||
<h1>
|
||||
<img src="~/duende-logo.svg" class="logo">
|
||||
Welcome to Duende IdentityServer
|
||||
<small class="text-muted">(version @Model.Version)</small>
|
||||
</h1>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
IdentityServer publishes a
|
||||
<a href="~/.well-known/openid-configuration">discovery document</a>
|
||||
where you can find metadata and links to all the endpoints, key material, etc.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/diagnostics">here</a> to see the claims for your current session.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/grants">here</a> to manage your stored grants.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/serversidesessions">here</a> to view the server side sessions.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/ciba/all">here</a> to view your pending CIBA login requests.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/admin">here</a> to view the simple admin page.
|
||||
</li>
|
||||
<li>
|
||||
Click <a href="~/portal">here</a> to view the client application portal.
|
||||
</li>
|
||||
<li>
|
||||
Here are links to the
|
||||
<a href="https://github.com/duendesoftware/IdentityServer">source code repository</a>,
|
||||
and <a href="https://github.com/duendesoftware/samples">ready to use samples</a>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if(Model.License != null)
|
||||
{
|
||||
<h2>License</h2>
|
||||
<dl>
|
||||
<dt>Serial Number</dt>
|
||||
<dd>@Model.License.SerialNumber</dd>
|
||||
<dt>Expiration</dt>
|
||||
<dd>@Model.License.Expiration!.Value.ToLongDateString()</dd>
|
||||
</dl>
|
||||
}
|
||||
</div>
|
||||
29
src/Microser/Microser.IdS/Pages/Index.cshtml.cs
Normal file
29
src/Microser/Microser.IdS/Pages/Index.cshtml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microser.IdS.Pages.Home
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
public Index(IdentityServerLicense? license = null)
|
||||
{
|
||||
License = license;
|
||||
}
|
||||
|
||||
public string Version
|
||||
{
|
||||
get => typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion.Split('+').First()
|
||||
?? "unavailable";
|
||||
}
|
||||
|
||||
public IdentityServerLicense? License { get; }
|
||||
}
|
||||
}
|
||||
86
src/Microser/Microser.IdS/Pages/Log.cs
Normal file
86
src/Microser/Microser.IdS/Pages/Log.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages
|
||||
{
|
||||
internal static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, string?, Exception?> _invalidId = LoggerMessage.Define<string?>(
|
||||
LogLevel.Error,
|
||||
EventIds.InvalidId,
|
||||
"Invalid id {Id}");
|
||||
|
||||
public static void InvalidId(this ILogger logger, string? id)
|
||||
{
|
||||
_invalidId(logger, id, null);
|
||||
}
|
||||
|
||||
private static readonly Action<ILogger, string?, Exception?> _invalidBackchannelLoginId = LoggerMessage.Define<string?>(
|
||||
LogLevel.Warning,
|
||||
EventIds.InvalidBackchannelLoginId,
|
||||
"Invalid backchannel login id {Id}");
|
||||
|
||||
public static void InvalidBackchannelLoginId(this ILogger logger, string? id)
|
||||
{
|
||||
_invalidBackchannelLoginId(logger, id, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, IEnumerable<string>, Exception?> _externalClaims = LoggerMessage.Define<IEnumerable<string>>(
|
||||
LogLevel.Debug,
|
||||
EventIds.ExternalClaims,
|
||||
"External claims: {Claims}");
|
||||
|
||||
public static void ExternalClaims(this ILogger logger, IEnumerable<string> claims)
|
||||
{
|
||||
_externalClaims(logger, claims, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, string, Exception?> _noMatchingBackchannelLoginRequest = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.NoMatchingBackchannelLoginRequest,
|
||||
"No backchannel login request matching id: {Id}");
|
||||
|
||||
public static void NoMatchingBackchannelLoginRequest(this ILogger logger, string id)
|
||||
{
|
||||
_noMatchingBackchannelLoginRequest(logger, id, null);
|
||||
}
|
||||
|
||||
private static Action<ILogger, string, Exception?> _noConsentMatchingRequest = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.NoConsentMatchingRequest,
|
||||
"No consent request matching request: {ReturnUrl}");
|
||||
|
||||
public static void NoConsentMatchingRequest(this ILogger logger, string returnUrl)
|
||||
{
|
||||
_noConsentMatchingRequest(logger, returnUrl, null);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EventIds
|
||||
{
|
||||
private const int UIEventsStart = 10000;
|
||||
|
||||
//////////////////////////////
|
||||
// Consent
|
||||
//////////////////////////////
|
||||
private const int ConsentEventsStart = UIEventsStart + 1000;
|
||||
|
||||
public const int InvalidId = ConsentEventsStart + 0;
|
||||
public const int NoConsentMatchingRequest = ConsentEventsStart + 1;
|
||||
|
||||
//////////////////////////////
|
||||
// External Login
|
||||
//////////////////////////////
|
||||
private const int ExternalLoginEventsStart = UIEventsStart + 2000;
|
||||
|
||||
public const int ExternalClaims = ExternalLoginEventsStart + 0;
|
||||
|
||||
//////////////////////////////
|
||||
// CIBA
|
||||
//////////////////////////////
|
||||
private const int CibaEventsStart = UIEventsStart + 3000;
|
||||
|
||||
public const int InvalidBackchannelLoginId = CibaEventsStart + 0;
|
||||
public const int NoMatchingBackchannelLoginRequest = CibaEventsStart + 1;
|
||||
}
|
||||
}
|
||||
43
src/Microser/Microser.IdS/Pages/Portal/ClientRepository.cs
Normal file
43
src/Microser/Microser.IdS/Pages/Portal/ClientRepository.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Microser.IdS.Pages.Portal
|
||||
{
|
||||
public class ThirdPartyInitiatedLoginLink
|
||||
{
|
||||
public string? LinkText { get; set; }
|
||||
public string? InitiateLoginUri { get; set; }
|
||||
}
|
||||
|
||||
public class ClientRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public ClientRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ThirdPartyInitiatedLoginLink>> GetClientsWithLoginUris(string? filter = null)
|
||||
{
|
||||
var query = _context.Clients
|
||||
.Where(c => c.InitiateLoginUri != null);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.ClientId.Contains(filter) || x.ClientName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(c => new ThirdPartyInitiatedLoginLink
|
||||
{
|
||||
LinkText = string.IsNullOrWhiteSpace(c.ClientName) ? c.ClientId : c.ClientName,
|
||||
InitiateLoginUri = c.InitiateLoginUri
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Microser/Microser.IdS/Pages/Portal/Index.cshtml
Normal file
30
src/Microser/Microser.IdS/Pages/Portal/Index.cshtml
Normal file
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Portal.Index
|
||||
|
||||
<div class="portal-page">
|
||||
|
||||
<div class="lead">
|
||||
<h1>Client Application Portal</h1>
|
||||
<p>This portal contains links to client applications that are configured
|
||||
with an InitiateLoginUri, which enables
|
||||
<a href="https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin">third-party initiated login</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if(!Model.Clients.Any())
|
||||
{
|
||||
<div class="alert">
|
||||
You do not have any clients configured with an InitiateLoginUri.
|
||||
</div>
|
||||
}
|
||||
|
||||
<ul class="list-group">
|
||||
@foreach (var client in Model.Clients)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<a href="@client.InitiateLoginUri">@client.LinkText</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
23
src/Microser/Microser.IdS/Pages/Portal/Index.cshtml.cs
Normal file
23
src/Microser/Microser.IdS/Pages/Portal/Index.cshtml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Portal
|
||||
{
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
public IEnumerable<ThirdPartyInitiatedLoginLink> Clients { get; private set; } = default!;
|
||||
|
||||
public Index(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Clients = await _repository.GetClientsWithLoginUris();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Microser/Microser.IdS/Pages/Redirect/Index.cshtml
Normal file
14
src/Microser/Microser.IdS/Pages/Redirect/Index.cshtml
Normal file
@@ -0,0 +1,14 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Redirect.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="redirect-page">
|
||||
<div class="lead">
|
||||
<h1>You are now being returned to the application</h1>
|
||||
<p>Once complete, you may close this tab.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<meta http-equiv="refresh" content="0;url=@Model.RedirectUri" data-url="@Model.RedirectUri">
|
||||
<script src="~/js/signin-redirect.js"></script>
|
||||
26
src/Microser/Microser.IdS/Pages/Redirect/Index.cshtml.cs
Normal file
26
src/Microser/Microser.IdS/Pages/Redirect/Index.cshtml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Redirect
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public string? RedirectUri { get; set; }
|
||||
|
||||
public IActionResult OnGet(string? redirectUri)
|
||||
{
|
||||
if (!Url.IsLocalUrl(redirectUri))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
RedirectUri = redirectUri;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user