added Microser

This commit is contained in:
M. Akif Tokatlioglu
2024-03-13 23:25:54 +03:00
parent d11ec09c4e
commit 7ae76afee4
208 changed files with 68884 additions and 0 deletions

View 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" }
},
};
}
}

View 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;
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>

View 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;

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View 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;

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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
}
}
}

View 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>

View File

@@ -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()
{
}
}
}

View 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>

View 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();
}
}
}

View 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; }
}
}

View 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>

View 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()
};
}
}
}

View 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; }
}
}

View 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.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";
}
}

View 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; }
}
}
}

View 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>

View 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 });
}
}
}

View File

@@ -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>
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View 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>

View File

@@ -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();
}
}
}

View 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>

View File

@@ -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);
}
}
}

View 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>

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View 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>

View 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();
}
}
}

View 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>

View File

@@ -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);
}
}
}

View 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>

View 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();
}
}
}

View File

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

View 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.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();
}
}
}

View File

@@ -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();
}
}
}

View File

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

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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();
}
}
}

View 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>

View 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()
{
}
}
}

View 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>

View 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();
}
}
}

View 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>

View 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
};
}
}
}

View 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";
}
}

View 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>

View 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();
}
}
}

View 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; }
}
}

View 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; }
}
}

View 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>

View 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";
}
}

View 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>

View 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
};
}
}
}

View 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; }
}
}

View 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; }
}
}

View File

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

View 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";
}
}

View 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>
}

View 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
};
}
}
}

View 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; }
}
}

View 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>

View 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()
{
}
}
}

View 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; }
}
}

View 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>

View 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>

View 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();
}
}
}

View 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; }
}
}

View 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 });
}
}
}

View File

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

View 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 } });
}
}
}
}

View File

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

View File

@@ -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);
}
}
}

View 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>

View 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");
}
}
}

View 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>();
}
}

View 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>

View 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;
}
}
}
}
}

View 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; }
}
}

View File

@@ -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")]

View 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>

View 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; }
}
}

View 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;
}
}

View 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();
}
}
}

View 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>

View 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();
}
}
}

View 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>

View 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