added Microser
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
using Microser.Core.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Microser.API.Weather.Controllers;
|
||||||
|
|
||||||
|
[Authorize(Policy = "ClientIdPolicy")]
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class WeatherForecastController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<WeatherForecastController> _logger;
|
||||||
|
|
||||||
|
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet(Name = "GetWeatherForecasts")]
|
||||||
|
public ActionResult<IEnumerable<WeatherForecast>> Get()
|
||||||
|
{
|
||||||
|
return Ok(Data.WeatherForecasts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}", Name = "GetWeatherForecast")]
|
||||||
|
public ActionResult<WeatherForecast> Get(int id)
|
||||||
|
{
|
||||||
|
var item = Data.WeatherForecasts.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (item == null)
|
||||||
|
return NotFound();
|
||||||
|
return Ok(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost(Name = "PostWeatherForecast")]
|
||||||
|
public ActionResult<WeatherForecast> Post(WeatherForecast weatherForecast)
|
||||||
|
{
|
||||||
|
var last = Data.WeatherForecasts.LastOrDefault();
|
||||||
|
if (last == null)
|
||||||
|
weatherForecast.Id = 1;
|
||||||
|
else
|
||||||
|
weatherForecast.Id = last.Id + 1;
|
||||||
|
|
||||||
|
Data.WeatherForecasts.Add(weatherForecast);
|
||||||
|
|
||||||
|
return new CreatedAtRouteResult("GetWeatherForecast", new { id = weatherForecast.Id }, weatherForecast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}", Name = "PutWeatherForecast")]
|
||||||
|
public ActionResult<WeatherForecast> Put(int id, WeatherForecast weatherForecast)
|
||||||
|
{
|
||||||
|
var item = Data.WeatherForecasts.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (item == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
item.Date = weatherForecast.Date;
|
||||||
|
item.TemperatureC = weatherForecast.TemperatureC;
|
||||||
|
item.Summary = weatherForecast.Summary;
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}", Name = "DeleteWeatherForecast")]
|
||||||
|
public ActionResult<WeatherForecast> Delete(int id)
|
||||||
|
{
|
||||||
|
var item = Data.WeatherForecasts.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (item == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
Data.WeatherForecasts.Remove(item);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using Microser.Core.Models;
|
||||||
|
|
||||||
|
namespace Microser.API.Weather;
|
||||||
|
|
||||||
|
public class Data
|
||||||
|
{
|
||||||
|
public static readonly string[] Summaries = new[]
|
||||||
|
{
|
||||||
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<WeatherForecast>? weatherForecasts;
|
||||||
|
|
||||||
|
public static List<WeatherForecast> WeatherForecasts
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (weatherForecasts == null)
|
||||||
|
weatherForecasts = GetWeatherForecasts();
|
||||||
|
return weatherForecasts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<WeatherForecast> GetWeatherForecasts()
|
||||||
|
{
|
||||||
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Id = index,
|
||||||
|
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||||
|
TemperatureC = Random.Shared.Next(-20, 55),
|
||||||
|
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microser.Core\Microser.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@Microser.API.Weather_HostAddress = http://localhost:7001
|
||||||
|
|
||||||
|
GET {{Microser.API.Weather_HostAddress}}/weatherforecast
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET {{Microser.API.Weather_HostAddress}}/weatherforecast/1
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
POST {{Microser.API.Weather_HostAddress}}/weatherforecast
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"temperatureC": -99,
|
||||||
|
"summary": "Cool"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
PUT {{Microser.API.Weather_HostAddress}}/weatherforecast/1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"temperatureC": 111,
|
||||||
|
"summary": "AAAA"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
DELETE {{Microser.API.Weather_HostAddress}}/weatherforecast/1
|
||||||
|
|
||||||
|
###
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication("Bearer")
|
||||||
|
.AddJwtBearer("Bearer", options =>
|
||||||
|
{
|
||||||
|
options.Authority = "https://localhost:5001";
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateAudience = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("ClientIdPolicy", policy => policy.RequireClaim("client_id", "microser_api_weather", "dotnet_blazor_serverapp"));
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:6001",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="app.css" />
|
||||||
|
<link rel="stylesheet" href="Microser.BlazorAppClient.styles.css" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<HeadOutlet @rendermode="InteractiveServer" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes @rendermode="InteractiveServer" />
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<div class="sidebar">
|
||||||
|
<NavMenu />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="top-row px-4">
|
||||||
|
|
||||||
|
<LoginDisplay />
|
||||||
|
|
||||||
|
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="content px-4">
|
||||||
|
@Body
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">Microser.BlazorAppClient</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||||
|
|
||||||
|
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||||
|
<nav class="flex-column">
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="Profile">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Profile
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="OnlyAdmin">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> OnlyAdmin
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="WeatherForecast">
|
||||||
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> WeatherForecast
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
top: -1px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-house-door-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-plus-square-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-list-nested-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link {
|
||||||
|
color: #d7d7d7;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(255,255,255,0.37);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked ~ .nav-scrollable {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<a href="profile">Hello, @context.User.Identity.Name !</a>
|
||||||
|
|
||||||
|
<form action="Account/Logout" method="post">
|
||||||
|
<AntiforgeryToken />
|
||||||
|
@* <input type="hidden" name="ReturnUrl" value="@currentUrl" /> *@
|
||||||
|
@* <button type="submit" class="dropdown-item notify-item">
|
||||||
|
<span>Logout</span>
|
||||||
|
</button> *@
|
||||||
|
<a href="#" onclick="this.parentNode.submit();">Logout</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<a href="Account/login?redirectUri=/">Log in</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@page "/AccessDenied"
|
||||||
|
|
||||||
|
<PageTitle>AccessDenied</PageTitle>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1>Access Denied</h1>
|
||||||
|
<p>You do not have permission to access that resource.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
[CascadingParameter]
|
||||||
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@page "/"
|
||||||
|
|
||||||
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
|
||||||
|
Welcome to your new app.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
@page "/OnlyAdmin"
|
||||||
|
|
||||||
|
@using System.Net.Http
|
||||||
|
@using Microser.BlazorAppClient.Services
|
||||||
|
@using Microsoft.AspNetCore.Authentication
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
|
@using IdentityModel.Client
|
||||||
|
@using Microsoft.IdentityModel.Protocols.OpenIdConnect
|
||||||
|
|
||||||
|
@inject IHttpClientFactory HttpClientFactory
|
||||||
|
@inject IHttpContextAccessor HttpContextAccessor
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IWeatherForecastApiService WeatherForecastApiService
|
||||||
|
|
||||||
|
@attribute [Authorize(Roles = "Admin")]
|
||||||
|
|
||||||
|
<PageTitle>OnlyAdmin</PageTitle>
|
||||||
|
|
||||||
|
<h4>OnlyAdmin</h4>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from a service.</p>
|
||||||
|
|
||||||
|
<p>IdentityAdmin Part:</p>
|
||||||
|
<AuthorizeView Roles="IdentityAdmin">
|
||||||
|
<Authorized>
|
||||||
|
<a href="https://localhost:5001/admin">Identity Admin Panel</a>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<p>Not authorized: need IdentityAdmin role</p>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
<p>Admin Part:</p>
|
||||||
|
@if (UserInfoList == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Claim Type
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Claim Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in UserInfoList)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@item.Type
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@item.Value
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
List<(string Type, string Value)>? UserInfoList;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idpClient = HttpClientFactory.CreateClient("IDPClient");
|
||||||
|
|
||||||
|
var metadataResponse = await idpClient.GetDiscoveryDocumentAsync();
|
||||||
|
if (metadataResponse.IsError)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Something went wrong while requesting the access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? accessToken;
|
||||||
|
accessToken = await HttpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var userInfoResponse = await idpClient.GetUserInfoAsync(
|
||||||
|
new UserInfoRequest
|
||||||
|
{
|
||||||
|
Address = metadataResponse.UserInfoEndpoint,
|
||||||
|
Token = accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userInfoResponse.IsError)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Something went wrong while requesting the access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfoList = new List<(string Type, string Value)>();
|
||||||
|
foreach (var claim in userInfoResponse.Claims)
|
||||||
|
{
|
||||||
|
userInfoList.Add((claim.Type, claim.Value));
|
||||||
|
}
|
||||||
|
UserInfoList = userInfoList;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
@page "/Profile"
|
||||||
|
|
||||||
|
@using System.Net.Http
|
||||||
|
@using Microser.BlazorAppClient.Services
|
||||||
|
@using Microsoft.AspNetCore.Authentication
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
|
@using IdentityModel.Client
|
||||||
|
@using Microsoft.IdentityModel.Protocols.OpenIdConnect
|
||||||
|
|
||||||
|
@inject IHttpClientFactory HttpClientFactory
|
||||||
|
@inject IHttpContextAccessor HttpContextAccessor
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IWeatherForecastApiService WeatherForecastApiService
|
||||||
|
|
||||||
|
@attribute [Authorize]
|
||||||
|
|
||||||
|
<PageTitle>Profile</PageTitle>
|
||||||
|
|
||||||
|
<h4>Profile</h4>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from a service.</p>
|
||||||
|
|
||||||
|
@if (UserInfoList == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Claim Type
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Claim Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in UserInfoList)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@item.Type
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@item.Value
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
List<(string Type, string Value)>? UserInfoList;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var idpClient = HttpClientFactory.CreateClient("IDPClient");
|
||||||
|
|
||||||
|
var metadataResponse = await idpClient.GetDiscoveryDocumentAsync();
|
||||||
|
if (metadataResponse.IsError)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Something went wrong while requesting the access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
string? accessToken;
|
||||||
|
accessToken = await HttpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var userInfoResponse = await idpClient.GetUserInfoAsync(
|
||||||
|
new UserInfoRequest
|
||||||
|
{
|
||||||
|
Address = metadataResponse.UserInfoEndpoint,
|
||||||
|
Token = accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userInfoResponse.IsError)
|
||||||
|
{
|
||||||
|
throw new HttpRequestException("Something went wrong while requesting the access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfoList = new List<(string Type, string Value)>();
|
||||||
|
foreach (var claim in userInfoResponse.Claims)
|
||||||
|
{
|
||||||
|
userInfoList.Add((claim.Type, claim.Value));
|
||||||
|
}
|
||||||
|
UserInfoList = userInfoList;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
@page "/WeatherForecast/Create"
|
||||||
|
|
||||||
|
<PageTitle>Create</PageTitle>
|
||||||
|
|
||||||
|
<h1>Create</h1>
|
||||||
|
<h4>WeatherForecast</h4>
|
||||||
|
<hr />
|
||||||
|
@if (weatherForecast == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<EditForm Model="@weatherForecast" OnValidSubmit="@HandleValidSubmit" Context="createWeatherForecast">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.Date)</label>
|
||||||
|
<InputDate @bind-Value="weatherForecast.Date" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.Date)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.TemperatureC)</label>
|
||||||
|
<InputNumber @bind-Value="weatherForecast.TemperatureC" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.TemperatureC)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.Summary)</label>
|
||||||
|
<InputText @bind-Value="weatherForecast.Summary" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.Summary)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Create" class="btn btn-primary" />
|
||||||
|
</div>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/WeatherForecast">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private WeatherForecast? weatherForecast;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
weatherForecast = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleValidSubmit()
|
||||||
|
{
|
||||||
|
if (weatherForecast is null) return;
|
||||||
|
|
||||||
|
var result = await WeatherForecastApiService.AddAsync(weatherForecast);
|
||||||
|
if (result is not null)
|
||||||
|
NavigationManager.NavigateTo("/WeatherForecast");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
@page "/WeatherForecast/Delete/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Delete</PageTitle>
|
||||||
|
|
||||||
|
<h1>Delete</h1>
|
||||||
|
|
||||||
|
<h3>Are you sure you want to delete this?</h3>
|
||||||
|
|
||||||
|
@if (weatherForecast == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<h4>WeatherForecast</h4>
|
||||||
|
<hr />
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Id)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Id
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Date)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Date
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.TemperatureC)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.TemperatureC
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.TemperatureF)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.TemperatureF
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Summary)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Summary
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-danger" @onclick="DeleteButtonClick">Delete</button> |
|
||||||
|
<a href="/WeatherForecast">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private WeatherForecast? weatherForecast;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (weatherForecast == null)
|
||||||
|
weatherForecast = await WeatherForecastApiService.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void DeleteButtonClick()
|
||||||
|
{
|
||||||
|
bool result = await WeatherForecastApiService.DeleteByIdAsync(id);
|
||||||
|
if (result)
|
||||||
|
NavigationManager.NavigateTo("/WeatherForecast");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@page "/WeatherForecast/Details/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Details</PageTitle>
|
||||||
|
|
||||||
|
<h1>Details</h1>
|
||||||
|
@if (weatherForecast == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<h4>WeatherForecast</h4>
|
||||||
|
<hr />
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Id)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Id
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Date)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Date
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.TemperatureC)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.TemperatureC
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.TemperatureF)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.TemperatureF
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-2">
|
||||||
|
@nameof(WeatherForecast.Summary)
|
||||||
|
</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@weatherForecast.Summary
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/WeatherForecast/Edit/@weatherForecast.Id">Edit</a> |
|
||||||
|
<a href="/WeatherForecast">Back to List</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private WeatherForecast? weatherForecast;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (weatherForecast == null)
|
||||||
|
weatherForecast = await WeatherForecastApiService.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
@page "/WeatherForecast/Edit/{id:int}"
|
||||||
|
|
||||||
|
<PageTitle>Edit</PageTitle>
|
||||||
|
|
||||||
|
<h1>Edit</h1>
|
||||||
|
<h4>WeatherForecast</h4>
|
||||||
|
<hr />
|
||||||
|
@if (weatherForecast == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<EditForm Model="@weatherForecast" OnValidSubmit="@HandleValidSubmit" Context="editWeatherForecast">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.Date)</label>
|
||||||
|
<InputDate @bind-Value="weatherForecast.Date" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.Date)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.TemperatureC)</label>
|
||||||
|
<InputNumber @bind-Value="weatherForecast.TemperatureC" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.TemperatureC)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">@nameof(WeatherForecast.Summary)</label>
|
||||||
|
<InputText @bind-Value="weatherForecast.Summary" class="form-control" />
|
||||||
|
<ValidationMessage For="@(() => weatherForecast.Summary)" class="text-danger" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="submit" value="Save" class="btn btn-primary" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/WeatherForecast">Back to List</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int id { get; set; }
|
||||||
|
|
||||||
|
private WeatherForecast? weatherForecast;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (weatherForecast == null)
|
||||||
|
weatherForecast = await WeatherForecastApiService.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleValidSubmit()
|
||||||
|
{
|
||||||
|
if (weatherForecast is null) return;
|
||||||
|
|
||||||
|
var result = await WeatherForecastApiService.UpdateAsync(id, weatherForecast);
|
||||||
|
if (result)
|
||||||
|
NavigationManager.NavigateTo("/WeatherForecast");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
@page "/WeatherForecast"
|
||||||
|
|
||||||
|
<PageTitle>WeatherForecasts</PageTitle>
|
||||||
|
|
||||||
|
<h4>WeatherForecasts</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="/WeatherForecast/Create">Create New</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from a service.</p>
|
||||||
|
|
||||||
|
@if (weatherForecasts == null)
|
||||||
|
{
|
||||||
|
<p><em>Loading...</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@nameof(WeatherForecast.Id)</th>
|
||||||
|
<th>@nameof(WeatherForecast.Date)</th>
|
||||||
|
<th>@nameof(WeatherForecast.TemperatureC)</th>
|
||||||
|
<th>@nameof(WeatherForecast.TemperatureF)</th>
|
||||||
|
<th>@nameof(WeatherForecast.Summary)</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var weatherForecast in weatherForecasts)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@weatherForecast.Id</td>
|
||||||
|
<td>@weatherForecast.Date</td>
|
||||||
|
<td>@weatherForecast.TemperatureC</td>
|
||||||
|
<td>@weatherForecast.TemperatureF</td>
|
||||||
|
<td>@weatherForecast.Summary</td>
|
||||||
|
<td>
|
||||||
|
<a href="/WeatherForecast/Details/@weatherForecast.Id">Details</a> |
|
||||||
|
<a href="/WeatherForecast/Edit/@weatherForecast.Id">Edit</a> |
|
||||||
|
<a href="/WeatherForecast/Delete/@weatherForecast.Id">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private WeatherForecast[]? weatherForecasts;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
weatherForecasts = await WeatherForecastApiService.GetAllAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
NavigationManager.Refresh(forceReload: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@using Microser.BlazorAppClient.Services
|
||||||
|
@using Microser.Core.Models
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject IWeatherForecastApiService WeatherForecastApiService
|
||||||
|
|
||||||
|
@attribute [Authorize]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@* <Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
|
*@
|
||||||
|
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
@{
|
||||||
|
// var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
|
||||||
|
var returnUrl = NavigationManager.Uri;
|
||||||
|
NavigationManager.NavigateTo($"Account/login?redirectUri={returnUrl}", forceLoad: true);
|
||||||
|
}
|
||||||
|
</NotAuthorized>
|
||||||
|
<Authorizing>
|
||||||
|
Wait...
|
||||||
|
</Authorizing>
|
||||||
|
</AuthorizeRouteView>
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<LayoutView Layout="@typeof(Layout.MainLayout)">
|
||||||
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using Microser.BlazorAppClient
|
||||||
|
@using Microser.BlazorAppClient.Components
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Microser.BlazorAppClient.Extensions;
|
||||||
|
|
||||||
|
public static class HttpClientExtensions
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
public static async Task<T> ReadContentAs<T>(this HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}");
|
||||||
|
|
||||||
|
var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(dataAsString, jsonSerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<HttpResponseMessage> PostAsJson<T>(this HttpClient httpClient, string url, T data)
|
||||||
|
{
|
||||||
|
var dataAsString = JsonSerializer.Serialize(data);
|
||||||
|
var content = new StringContent(dataAsString);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
return httpClient.PostAsync(url, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<HttpResponseMessage> PutAsJson<T>(this HttpClient httpClient, string url, T data)
|
||||||
|
{
|
||||||
|
var dataAsString = JsonSerializer.Serialize(data);
|
||||||
|
var content = new StringContent(dataAsString);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
|
||||||
|
return httpClient.PutAsync(url, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using IdentityModel.Client;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||||
|
|
||||||
|
namespace Microser.BlazorAppClient.HttpHandlers;
|
||||||
|
|
||||||
|
public class AuthenticationDelegatingHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
public AuthenticationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(accessToken))
|
||||||
|
{
|
||||||
|
request.SetBearerToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="IdentityModel" Version="6.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microser.Core\Microser.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using Microser.BlazorAppClient;
|
||||||
|
using Microser.BlazorAppClient.Components;
|
||||||
|
using Microser.BlazorAppClient.HttpHandlers;
|
||||||
|
using Microser.BlazorAppClient.Services;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddRazorComponents()
|
||||||
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
|
builder.Services.AddTransient<AuthenticationDelegatingHandler>();
|
||||||
|
builder.Services.AddScoped<IWeatherForecastApiService, WeatherForecastApiService>();
|
||||||
|
|
||||||
|
builder.Services.AddOIDCAuthentication();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClients();
|
||||||
|
|
||||||
|
builder.Services.AddPolicies();
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.MapPost("/account/logout", async (HttpContext context) =>
|
||||||
|
{
|
||||||
|
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/account/login", async (string redirectUri, HttpContext context) =>
|
||||||
|
{
|
||||||
|
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = redirectUri });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StartupExtensions
|
||||||
|
{
|
||||||
|
public static void AddOIDCAuthentication(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddAntiforgery(options => options.Cookie.Name = "ClientBlazorAppAntiForgeryCookie")
|
||||||
|
.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.Cookie.Name = "ClientBlazorAppAuthCookie";
|
||||||
|
options.AccessDeniedPath = "/AccessDenied";
|
||||||
|
})
|
||||||
|
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
|
||||||
|
{
|
||||||
|
options.Authority = "https://localhost:5001/";
|
||||||
|
options.ClientId = "dotnet_blazor_serverapp";
|
||||||
|
options.ClientSecret = "E8C65E41BB0E4E519D409023CF5112F4";
|
||||||
|
options.ResponseType = "code";
|
||||||
|
options.SaveTokens = true;
|
||||||
|
options.GetClaimsFromUserInfoEndpoint = true;
|
||||||
|
options.UseTokenLifetime = false;
|
||||||
|
|
||||||
|
//options.SignedOutRedirectUri = "/";
|
||||||
|
|
||||||
|
options.Scope.Add("openid");
|
||||||
|
options.Scope.Add("profile");
|
||||||
|
options.Scope.Add("address");
|
||||||
|
options.Scope.Add("email");
|
||||||
|
options.Scope.Add("roles");
|
||||||
|
|
||||||
|
options.Scope.Add("microser_api_weather");
|
||||||
|
|
||||||
|
options.ClaimActions.MapJsonKey("role", "role");
|
||||||
|
//options.ClaimActions.MapUniqueJsonKey("role", "role");
|
||||||
|
//options.ClaimActions.MapAll();
|
||||||
|
|
||||||
|
options.TokenValidationParameters = new
|
||||||
|
TokenValidationParameters
|
||||||
|
{
|
||||||
|
NameClaimType = "name",
|
||||||
|
RoleClaimType = "role"
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Events = new OpenIdConnectEvents
|
||||||
|
{
|
||||||
|
OnAccessDenied = context =>
|
||||||
|
{
|
||||||
|
context.HandleResponse();
|
||||||
|
context.Response.Redirect("/");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddHttpClients(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHttpClient("WeatherForecastAPIClient", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri("https://localhost:6001/");
|
||||||
|
client.DefaultRequestHeaders.Clear();
|
||||||
|
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
|
||||||
|
})
|
||||||
|
.AddHttpMessageHandler<AuthenticationDelegatingHandler>();
|
||||||
|
|
||||||
|
// added for get user info
|
||||||
|
services.AddHttpClient("IDPClient", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri("https://localhost:5001/");
|
||||||
|
client.DefaultRequestHeaders.Clear();
|
||||||
|
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddPolicies(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddAuthorization(opts =>
|
||||||
|
{
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.UserRolePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var role in ProjectPolicies.UserRolePolicy.RequiredRoles)
|
||||||
|
policy.RequireRole(role.RoleName);
|
||||||
|
});
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.IdentityAdminRolePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var role in ProjectPolicies.IdentityAdminRolePolicy.RequiredRoles)
|
||||||
|
policy.RequireRole(role.RoleName);
|
||||||
|
});
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.AdminRolePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var role in ProjectPolicies.AdminRolePolicy.RequiredRoles)
|
||||||
|
policy.RequireRole(role.RoleName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddAuthorization(opts =>
|
||||||
|
{
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.WeatherForecastCreatePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var claim in ProjectPolicies.WeatherForecastCreatePolicy.RequiredClaims)
|
||||||
|
policy.RequireClaim(claim.Type, new[] { claim.Value });
|
||||||
|
});
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.WeatherForecastReadPolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var claim in ProjectPolicies.WeatherForecastReadPolicy.RequiredClaims)
|
||||||
|
policy.RequireClaim(claim.Type, new[] { claim.Value });
|
||||||
|
});
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.WeatherForecastUpdatePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var claim in ProjectPolicies.WeatherForecastUpdatePolicy.RequiredClaims)
|
||||||
|
policy.RequireClaim(claim.Type, new[] { claim.Value });
|
||||||
|
});
|
||||||
|
opts.AddPolicy(nameof(ProjectPolicies.WeatherForecastDeletePolicy), policy =>
|
||||||
|
{
|
||||||
|
foreach (var claim in ProjectPolicies.WeatherForecastDeletePolicy.RequiredClaims)
|
||||||
|
policy.RequireClaim(claim.Type, new[] { claim.Value });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
namespace Microser.BlazorAppClient;
|
||||||
|
|
||||||
|
public static class ProjectRoles
|
||||||
|
{
|
||||||
|
public static ProjectRole UserRole => new ProjectRole { RoleName = "User" };
|
||||||
|
public static ProjectRole AdminRole => new ProjectRole { RoleName = "Admin" };
|
||||||
|
public static ProjectRole IdentityAdmin => new ProjectRole { RoleName = "IdentityAdmin" };
|
||||||
|
|
||||||
|
public static ProjectRole[] Roles => new ProjectRole[] { UserRole, AdminRole, IdentityAdmin };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProjectPolicies
|
||||||
|
{
|
||||||
|
public static ProjectPolicy UserRolePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(UserRolePolicy),
|
||||||
|
RequiredRoles = new[] { ProjectRoles.UserRole }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy IdentityAdminRolePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(IdentityAdminRolePolicy),
|
||||||
|
RequiredRoles = new[] { ProjectRoles.IdentityAdmin }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy AdminRolePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(AdminRolePolicy),
|
||||||
|
RequiredRoles = new[] { ProjectRoles.AdminRole }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy WeatherForecastCreatePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(WeatherForecastCreatePolicy),
|
||||||
|
RequiredClaims = new[] { ProjectClaims.WeatherForecastCreate }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy WeatherForecastReadPolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(WeatherForecastReadPolicy),
|
||||||
|
RequiredClaims = new[] { ProjectClaims.WeatherForecastRead }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy WeatherForecastUpdatePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(WeatherForecastUpdatePolicy),
|
||||||
|
RequiredClaims = new[] { ProjectClaims.WeatherForecastUpdate }
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProjectPolicy WeatherForecastDeletePolicy => new ProjectPolicy
|
||||||
|
{
|
||||||
|
Name = nameof(WeatherForecastDeletePolicy),
|
||||||
|
RequiredClaims = new[] { ProjectClaims.WeatherForecastDelete }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectPolicy
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public ProjectClaim[] RequiredClaims { get; set; }
|
||||||
|
public ProjectRole[] RequiredRoles { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProjectClaims
|
||||||
|
{
|
||||||
|
public static ProjectClaim WeatherForecastCreate => new ProjectClaim { Type = "WeatherForecast", Value = "Create" };
|
||||||
|
public static ProjectClaim WeatherForecastRead => new ProjectClaim { Type = "WeatherForecast", Value = "Read" };
|
||||||
|
public static ProjectClaim WeatherForecastUpdate => new ProjectClaim { Type = "WeatherForecast", Value = "Update" };
|
||||||
|
public static ProjectClaim WeatherForecastDelete => new ProjectClaim { Type = "WeatherForecast", Value = "Delete" };
|
||||||
|
|
||||||
|
public static List<ProjectClaim> GetMovieClaims => new List<ProjectClaim> { WeatherForecastCreate, WeatherForecastRead, WeatherForecastUpdate, WeatherForecastDelete };
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectClaim
|
||||||
|
{
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProjectRole
|
||||||
|
{
|
||||||
|
public string RoleName { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7001",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Microser.Core.Models;
|
||||||
|
|
||||||
|
namespace Microser.BlazorAppClient.Services;
|
||||||
|
|
||||||
|
public interface IWeatherForecastApiService
|
||||||
|
{
|
||||||
|
Task<WeatherForecast?> GetByIdAsync(int id);
|
||||||
|
|
||||||
|
Task<WeatherForecast[]?> GetAllAsync();
|
||||||
|
|
||||||
|
Task<WeatherForecast?> AddAsync(WeatherForecast weatherForecast);
|
||||||
|
|
||||||
|
Task<bool> UpdateAsync(int id, WeatherForecast weatherForecast);
|
||||||
|
|
||||||
|
Task<bool> DeleteByIdAsync(int id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using Microser.BlazorAppClient.Extensions;
|
||||||
|
using Microser.Core.Models;
|
||||||
|
|
||||||
|
namespace Microser.BlazorAppClient.Services;
|
||||||
|
|
||||||
|
public class WeatherForecastApiService : IWeatherForecastApiService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
public WeatherForecastApiService(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||||
|
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WeatherForecast[]?> GetAllAsync()
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient");
|
||||||
|
var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
"/WeatherForecast");
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
//var content = await response.Content.ReadAsStringAsync();
|
||||||
|
//var weatherForecastList = JsonSerializer.Deserialize<List<WeatherForecast>>(content);
|
||||||
|
|
||||||
|
var weatherForecastList = await response.ReadContentAs<WeatherForecast[]>();
|
||||||
|
return weatherForecastList;
|
||||||
|
|
||||||
|
#region Another Way
|
||||||
|
|
||||||
|
// added for testing
|
||||||
|
|
||||||
|
//var apiClientCredentials = new ClientCredentialsTokenRequest
|
||||||
|
//{
|
||||||
|
// Address = "https://localhost:5005/connect/token",
|
||||||
|
|
||||||
|
// ClientId = "movieClient",
|
||||||
|
// ClientSecret = "D04449B9D7BB46C7AF8B5951076115F3",
|
||||||
|
|
||||||
|
// Scope = "movieAPI"
|
||||||
|
//};
|
||||||
|
|
||||||
|
//var client = new HttpClient();
|
||||||
|
|
||||||
|
//var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5005");
|
||||||
|
//if (disco.IsError)
|
||||||
|
//{
|
||||||
|
// return null; // throw 500 error
|
||||||
|
//}
|
||||||
|
|
||||||
|
//var tokenResponse = await client.RequestClientCredentialsTokenAsync(apiClientCredentials);
|
||||||
|
//if (tokenResponse.IsError)
|
||||||
|
//{
|
||||||
|
// return null;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//var apiClient = new HttpClient();
|
||||||
|
|
||||||
|
//apiClient.SetBearerToken(tokenResponse.AccessToken);
|
||||||
|
|
||||||
|
//var response = await apiClient.GetAsync("https://localhost:6001/api/movies");
|
||||||
|
//response.EnsureSuccessStatusCode();
|
||||||
|
//var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
//List<Movie> movieList = JsonSerializer.Deserialize<List<Movie>>(content);
|
||||||
|
//return movieList;
|
||||||
|
|
||||||
|
#endregion Another Way
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WeatherForecast?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient");
|
||||||
|
var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
"/WeatherForecast/" + id);
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var item = await response.ReadContentAs<WeatherForecast>();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WeatherForecast?> AddAsync(WeatherForecast weatherForecast)
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient");
|
||||||
|
var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post, "/WeatherForecast")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(weatherForecast)
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
weatherForecast = await response.ReadContentAs<WeatherForecast>();
|
||||||
|
return weatherForecast;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteByIdAsync(int id)
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient");
|
||||||
|
var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Delete, "/WeatherForecast/" + id.ToString());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(int id, WeatherForecast weatherForecast)
|
||||||
|
{
|
||||||
|
var httpClient = _httpClientFactory.CreateClient("WeatherForecastAPIClient");
|
||||||
|
var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Put, "/WeatherForecast/" + weatherForecast.Id.ToString())
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(weatherForecast)
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
html, body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #006bb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: #e50000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.darker-border-checkbox.form-check-input {
|
||||||
|
border-color: #929292;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Microser.Core.Models;
|
||||||
|
|
||||||
|
public class WeatherForecast
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
@@ -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>
|
||||||
@@ -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;
|
||||||
|
|
||||||
Generated
+1067
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,721 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Microser.IdS.Migrations.ConfigurationDb
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Configuration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiResources",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||||
|
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RequireResourceIndicator = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiResources", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiScopes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||||
|
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiScopes", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Clients",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
ProtocolType = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
RequireClientSecret = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ClientName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||||
|
ClientUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
LogoUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
RequireConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AllowRememberConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RequirePkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AllowPlainTextPkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RequireRequestObject = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AllowAccessTokensViaBrowser = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RequireDPoP = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
DPoPValidationMode = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
DPoPClockSkew = table.Column<TimeSpan>(type: "TEXT", nullable: false),
|
||||||
|
FrontChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
BackChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
BackChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AllowOfflineAccess = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
IdentityTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
AccessTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
AuthorizationCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
ConsentLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
SlidingRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
RefreshTokenUsage = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RefreshTokenExpiration = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
AccessTokenType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
EnableLocalLogin = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
IncludeJwtId = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
AlwaysSendClientClaims = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ClientClaimsPrefix = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
PairWiseSubjectSalt = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
InitiateLoginUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
UserSsoLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
UserCodeType = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
DeviceCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
CibaLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
PollingInterval = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
CoordinateLifetimeWithUserSession = table.Column<bool>(type: "INTEGER", nullable: true),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
PushedAuthorizationLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
RequirePushedAuthorization = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Clients", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "IdentityProviders",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Scheme = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||||
|
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_IdentityProviders", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "IdentityResources",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||||
|
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_IdentityResources", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiResourceClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
|
||||||
|
column: x => x.ApiResourceId,
|
||||||
|
principalTable: "ApiResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiResourceProperties",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
|
||||||
|
column: x => x.ApiResourceId,
|
||||||
|
principalTable: "ApiResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiResourceScopes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
|
||||||
|
column: x => x.ApiResourceId,
|
||||||
|
principalTable: "ApiResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiResourceSecrets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
|
||||||
|
column: x => x.ApiResourceId,
|
||||||
|
principalTable: "ApiResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiScopeClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
|
||||||
|
column: x => x.ScopeId,
|
||||||
|
principalTable: "ApiScopes",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApiScopeProperties",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
|
||||||
|
column: x => x.ScopeId,
|
||||||
|
principalTable: "ApiScopes",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientClaims_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientCorsOrigins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Origin = table.Column<string>(type: "TEXT", maxLength: 150, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientCorsOrigins_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientGrantTypes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
GrantType = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientGrantTypes_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientIdPRestrictions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Provider = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientIdPRestrictions_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientPostLogoutRedirectUris",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
PostLogoutRedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientProperties",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientProperties", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientProperties_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientRedirectUris",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
RedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientRedirectUris_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientScopes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientScopes", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientScopes_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ClientSecrets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ClientSecrets", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ClientSecrets_Clients_ClientId",
|
||||||
|
column: x => x.ClientId,
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "IdentityResourceClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
|
||||||
|
column: x => x.IdentityResourceId,
|
||||||
|
principalTable: "IdentityResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "IdentityResourceProperties",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId",
|
||||||
|
column: x => x.IdentityResourceId,
|
||||||
|
principalTable: "IdentityResources",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiResourceClaims_ApiResourceId_Type",
|
||||||
|
table: "ApiResourceClaims",
|
||||||
|
columns: new[] { "ApiResourceId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiResourceProperties_ApiResourceId_Key",
|
||||||
|
table: "ApiResourceProperties",
|
||||||
|
columns: new[] { "ApiResourceId", "Key" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiResources_Name",
|
||||||
|
table: "ApiResources",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiResourceScopes_ApiResourceId_Scope",
|
||||||
|
table: "ApiResourceScopes",
|
||||||
|
columns: new[] { "ApiResourceId", "Scope" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiResourceSecrets_ApiResourceId",
|
||||||
|
table: "ApiResourceSecrets",
|
||||||
|
column: "ApiResourceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiScopeClaims_ScopeId_Type",
|
||||||
|
table: "ApiScopeClaims",
|
||||||
|
columns: new[] { "ScopeId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiScopeProperties_ScopeId_Key",
|
||||||
|
table: "ApiScopeProperties",
|
||||||
|
columns: new[] { "ScopeId", "Key" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApiScopes_Name",
|
||||||
|
table: "ApiScopes",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientClaims_ClientId_Type_Value",
|
||||||
|
table: "ClientClaims",
|
||||||
|
columns: new[] { "ClientId", "Type", "Value" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientCorsOrigins_ClientId_Origin",
|
||||||
|
table: "ClientCorsOrigins",
|
||||||
|
columns: new[] { "ClientId", "Origin" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientGrantTypes_ClientId_GrantType",
|
||||||
|
table: "ClientGrantTypes",
|
||||||
|
columns: new[] { "ClientId", "GrantType" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientIdPRestrictions_ClientId_Provider",
|
||||||
|
table: "ClientIdPRestrictions",
|
||||||
|
columns: new[] { "ClientId", "Provider" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri",
|
||||||
|
table: "ClientPostLogoutRedirectUris",
|
||||||
|
columns: new[] { "ClientId", "PostLogoutRedirectUri" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientProperties_ClientId_Key",
|
||||||
|
table: "ClientProperties",
|
||||||
|
columns: new[] { "ClientId", "Key" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientRedirectUris_ClientId_RedirectUri",
|
||||||
|
table: "ClientRedirectUris",
|
||||||
|
columns: new[] { "ClientId", "RedirectUri" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Clients_ClientId",
|
||||||
|
table: "Clients",
|
||||||
|
column: "ClientId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientScopes_ClientId_Scope",
|
||||||
|
table: "ClientScopes",
|
||||||
|
columns: new[] { "ClientId", "Scope" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ClientSecrets_ClientId",
|
||||||
|
table: "ClientSecrets",
|
||||||
|
column: "ClientId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_IdentityProviders_Scheme",
|
||||||
|
table: "IdentityProviders",
|
||||||
|
column: "Scheme",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_IdentityResourceClaims_IdentityResourceId_Type",
|
||||||
|
table: "IdentityResourceClaims",
|
||||||
|
columns: new[] { "IdentityResourceId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_IdentityResourceProperties_IdentityResourceId_Key",
|
||||||
|
table: "IdentityResourceProperties",
|
||||||
|
columns: new[] { "IdentityResourceId", "Key" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_IdentityResources_Name",
|
||||||
|
table: "IdentityResources",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiResourceClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiResourceProperties");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiResourceScopes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiResourceSecrets");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiScopeClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiScopeProperties");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientCorsOrigins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientGrantTypes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientIdPRestrictions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientPostLogoutRedirectUris");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientProperties");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientRedirectUris");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientScopes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ClientSecrets");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "IdentityProviders");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "IdentityResourceClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "IdentityResourceProperties");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiResources");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApiScopes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Clients");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "IdentityResources");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1064
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||||
|
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||||
|
"ProductVersion" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE "DeviceCodes" (
|
||||||
|
"UserCode" TEXT NOT NULL CONSTRAINT "PK_DeviceCodes" PRIMARY KEY,
|
||||||
|
"DeviceCode" TEXT NOT NULL,
|
||||||
|
"SubjectId" TEXT NULL,
|
||||||
|
"SessionId" TEXT NULL,
|
||||||
|
"ClientId" TEXT NOT NULL,
|
||||||
|
"Description" TEXT NULL,
|
||||||
|
"CreationTime" TEXT NOT NULL,
|
||||||
|
"Expiration" TEXT NOT NULL,
|
||||||
|
"Data" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "Keys" (
|
||||||
|
"Id" TEXT NOT NULL CONSTRAINT "PK_Keys" PRIMARY KEY,
|
||||||
|
"Version" INTEGER NOT NULL,
|
||||||
|
"Created" TEXT NOT NULL,
|
||||||
|
"Use" TEXT NULL,
|
||||||
|
"Algorithm" TEXT NOT NULL,
|
||||||
|
"IsX509Certificate" INTEGER NOT NULL,
|
||||||
|
"DataProtected" INTEGER NOT NULL,
|
||||||
|
"Data" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "PersistedGrants" (
|
||||||
|
"Id" INTEGER NOT NULL CONSTRAINT "PK_PersistedGrants" PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"Key" TEXT NULL,
|
||||||
|
"Type" TEXT NOT NULL,
|
||||||
|
"SubjectId" TEXT NULL,
|
||||||
|
"SessionId" TEXT NULL,
|
||||||
|
"ClientId" TEXT NOT NULL,
|
||||||
|
"Description" TEXT NULL,
|
||||||
|
"CreationTime" TEXT NOT NULL,
|
||||||
|
"Expiration" TEXT NULL,
|
||||||
|
"ConsumedTime" TEXT NULL,
|
||||||
|
"Data" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "PushedAuthorizationRequests" (
|
||||||
|
"Id" INTEGER NOT NULL CONSTRAINT "PK_PushedAuthorizationRequests" PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"ReferenceValueHash" TEXT NOT NULL,
|
||||||
|
"ExpiresAtUtc" TEXT NOT NULL,
|
||||||
|
"Parameters" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "ServerSideSessions" (
|
||||||
|
"Id" INTEGER NOT NULL CONSTRAINT "PK_ServerSideSessions" PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"Key" TEXT NOT NULL,
|
||||||
|
"Scheme" TEXT NOT NULL,
|
||||||
|
"SubjectId" TEXT NOT NULL,
|
||||||
|
"SessionId" TEXT NULL,
|
||||||
|
"DisplayName" TEXT NULL,
|
||||||
|
"Created" TEXT NOT NULL,
|
||||||
|
"Renewed" TEXT NOT NULL,
|
||||||
|
"Expires" TEXT NULL,
|
||||||
|
"Data" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_DeviceCodes_DeviceCode" ON "DeviceCodes" ("DeviceCode");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_DeviceCodes_Expiration" ON "DeviceCodes" ("Expiration");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_Keys_Use" ON "Keys" ("Use");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_PersistedGrants_ConsumedTime" ON "PersistedGrants" ("ConsumedTime");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_PersistedGrants_Expiration" ON "PersistedGrants" ("Expiration");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_PersistedGrants_Key" ON "PersistedGrants" ("Key");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_PersistedGrants_SubjectId_ClientId_Type" ON "PersistedGrants" ("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_PersistedGrants_SubjectId_SessionId_Type" ON "PersistedGrants" ("SubjectId", "SessionId", "Type");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_PushedAuthorizationRequests_ExpiresAtUtc" ON "PushedAuthorizationRequests" ("ExpiresAtUtc");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_PushedAuthorizationRequests_ReferenceValueHash" ON "PushedAuthorizationRequests" ("ReferenceValueHash");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_ServerSideSessions_DisplayName" ON "ServerSideSessions" ("DisplayName");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_ServerSideSessions_Expires" ON "ServerSideSessions" ("Expires");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "IX_ServerSideSessions_Key" ON "ServerSideSessions" ("Key");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_ServerSideSessions_SessionId" ON "ServerSideSessions" ("SessionId");
|
||||||
|
|
||||||
|
CREATE INDEX "IX_ServerSideSessions_SubjectId" ON "ServerSideSessions" ("SubjectId");
|
||||||
|
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20240312131625_Grants', '8.0.0');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PersistedGrantDbContext))]
|
||||||
|
[Migration("20240312131625_Grants")]
|
||||||
|
partial class Grants
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserCode")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DeviceCode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("UserCode");
|
||||||
|
|
||||||
|
b.HasIndex("DeviceCode")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.ToTable("DeviceCodes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Algorithm")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("DataProtected")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("IsX509Certificate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Use")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Use");
|
||||||
|
|
||||||
|
b.ToTable("Keys", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ConsumedTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ConsumedTime");
|
||||||
|
|
||||||
|
b.HasIndex("Expiration");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||||
|
|
||||||
|
b.ToTable("PersistedGrants", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAtUtc")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Parameters")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ReferenceValueHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ExpiresAtUtc");
|
||||||
|
|
||||||
|
b.HasIndex("ReferenceValueHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("PushedAuthorizationRequests", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("Expires")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Renewed")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Scheme")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SessionId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DisplayName");
|
||||||
|
|
||||||
|
b.HasIndex("Expires");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SessionId");
|
||||||
|
|
||||||
|
b.HasIndex("SubjectId");
|
||||||
|
|
||||||
|
b.ToTable("ServerSideSessions", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class Grants : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "DeviceCodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
DeviceCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Keys",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Version = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Use = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Algorithm = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
IsX509Certificate = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
DataProtected = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Keys", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PersistedGrants",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||||
|
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
ConsumedTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PersistedGrants", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PushedAuthorizationRequests",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ReferenceValueHash = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||||
|
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Parameters = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PushedAuthorizationRequests", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ServerSideSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Key = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
Scheme = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
SubjectId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||||
|
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
DisplayName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Renewed = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
Expires = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ServerSideSessions", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_DeviceCode",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "DeviceCode",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DeviceCodes_Expiration",
|
||||||
|
table: "DeviceCodes",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Keys_Use",
|
||||||
|
table: "Keys",
|
||||||
|
column: "Use");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_ConsumedTime",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
column: "ConsumedTime");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_Expiration",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
column: "Expiration");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_Key",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
|
||||||
|
table: "PersistedGrants",
|
||||||
|
columns: new[] { "SubjectId", "SessionId", "Type" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PushedAuthorizationRequests_ExpiresAtUtc",
|
||||||
|
table: "PushedAuthorizationRequests",
|
||||||
|
column: "ExpiresAtUtc");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PushedAuthorizationRequests_ReferenceValueHash",
|
||||||
|
table: "PushedAuthorizationRequests",
|
||||||
|
column: "ReferenceValueHash",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServerSideSessions_DisplayName",
|
||||||
|
table: "ServerSideSessions",
|
||||||
|
column: "DisplayName");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServerSideSessions_Expires",
|
||||||
|
table: "ServerSideSessions",
|
||||||
|
column: "Expires");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServerSideSessions_Key",
|
||||||
|
table: "ServerSideSessions",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServerSideSessions_SessionId",
|
||||||
|
table: "ServerSideSessions",
|
||||||
|
column: "SessionId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServerSideSessions_SubjectId",
|
||||||
|
table: "ServerSideSessions",
|
||||||
|
column: "SubjectId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "DeviceCodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Keys");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PersistedGrants");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PushedAuthorizationRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ServerSideSessions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+256
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Account.AccessDeniedModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1>Access Denied</h1>
|
||||||
|
<p>You do not have permission to access that resource.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Account
|
||||||
|
{
|
||||||
|
public class AccessDeniedModel : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Login
|
||||||
|
{
|
||||||
|
public class InputModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
public bool RememberLogin { get; set; }
|
||||||
|
public string? ReturnUrl { get; set; }
|
||||||
|
public string? Button { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Login
|
||||||
|
{
|
||||||
|
public static class LoginOptions
|
||||||
|
{
|
||||||
|
public static readonly bool AllowLocalLogin = true;
|
||||||
|
public static readonly bool AllowRememberLogin = true;
|
||||||
|
public static readonly TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
|
||||||
|
public static readonly string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Duende.IdentityServer.Events;
|
||||||
|
using Duende.IdentityServer.Extensions;
|
||||||
|
using Duende.IdentityServer.Services;
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Logout
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class Index : PageModel
|
||||||
|
{
|
||||||
|
private readonly IIdentityServerInteractionService _interaction;
|
||||||
|
private readonly IEventService _events;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public string? LogoutId { get; set; }
|
||||||
|
|
||||||
|
public Index(IIdentityServerInteractionService interaction, IEventService events)
|
||||||
|
{
|
||||||
|
_interaction = interaction;
|
||||||
|
_events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGet(string? logoutId)
|
||||||
|
{
|
||||||
|
LogoutId = logoutId;
|
||||||
|
|
||||||
|
var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;
|
||||||
|
|
||||||
|
if (User.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
// if the user is not authenticated, then just show logged out page
|
||||||
|
showLogoutPrompt = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var context = await _interaction.GetLogoutContextAsync(LogoutId);
|
||||||
|
if (context?.ShowSignoutPrompt == false)
|
||||||
|
{
|
||||||
|
// it's safe to automatically sign-out
|
||||||
|
showLogoutPrompt = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLogoutPrompt == false)
|
||||||
|
{
|
||||||
|
// if the request for logout was properly authenticated from IdentityServer, then
|
||||||
|
// we don't need to show the prompt and can just log the user out directly.
|
||||||
|
return await OnPost();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPost()
|
||||||
|
{
|
||||||
|
if (User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
// if there's no current logout context, we need to create one
|
||||||
|
// this captures necessary info from the current logged in user
|
||||||
|
// this can still return null if there is no context needed
|
||||||
|
LogoutId ??= await _interaction.CreateLogoutContextAsync();
|
||||||
|
|
||||||
|
// delete local authentication cookie
|
||||||
|
await HttpContext.SignOutAsync();
|
||||||
|
|
||||||
|
// see if we need to trigger federated logout
|
||||||
|
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||||
|
|
||||||
|
// raise the logout event
|
||||||
|
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
|
||||||
|
Telemetry.Metrics.UserLogout(idp);
|
||||||
|
|
||||||
|
// if it's a local login we can ignore this workflow
|
||||||
|
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
|
||||||
|
{
|
||||||
|
// we need to see if the provider supports external logout
|
||||||
|
if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
|
||||||
|
{
|
||||||
|
// build a return URL so the upstream provider will redirect back
|
||||||
|
// to us after the user has logged out. this allows us to then
|
||||||
|
// complete our single sign-out processing.
|
||||||
|
var url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
|
||||||
|
|
||||||
|
// this triggers a redirect to the external provider for sign-out
|
||||||
|
return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Logout.LoggedOut
|
||||||
|
|
||||||
|
<div class="logged-out-page">
|
||||||
|
<h1>
|
||||||
|
Logout
|
||||||
|
<small>You are now logged out</small>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
@if (Model.View.PostLogoutRedirectUri != null)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
Click <a class="PostLogoutRedirectUri" href="@Model.View.PostLogoutRedirectUri">here</a> to return to the
|
||||||
|
<span>@Model.View.ClientName</span> application.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Model.View.SignOutIframeUrl != null)
|
||||||
|
{
|
||||||
|
<iframe width="0" height="0" class="signout" src="@Model.View.SignOutIframeUrl"></iframe>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section scripts
|
||||||
|
{
|
||||||
|
@if (Model.View.AutomaticRedirectAfterSignOut)
|
||||||
|
{
|
||||||
|
<script src="~/js/signout-redirect.js"></script>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Duende.IdentityServer.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Logout
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class LoggedOut : PageModel
|
||||||
|
{
|
||||||
|
private readonly IIdentityServerInteractionService _interactionService;
|
||||||
|
|
||||||
|
public LoggedOutViewModel View { get; set; } = default!;
|
||||||
|
|
||||||
|
public LoggedOut(IIdentityServerInteractionService interactionService)
|
||||||
|
{
|
||||||
|
_interactionService = interactionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnGet(string? logoutId)
|
||||||
|
{
|
||||||
|
// get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||||
|
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
|
||||||
|
|
||||||
|
View = new LoggedOutViewModel
|
||||||
|
{
|
||||||
|
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
|
||||||
|
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
|
||||||
|
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
|
||||||
|
SignOutIframeUrl = logout?.SignOutIFrameUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Logout
|
||||||
|
{
|
||||||
|
public class LoggedOutViewModel
|
||||||
|
{
|
||||||
|
public string? PostLogoutRedirectUri { get; set; }
|
||||||
|
public string? ClientName { get; set; }
|
||||||
|
public string? SignOutIframeUrl { get; set; }
|
||||||
|
public bool AutomaticRedirectAfterSignOut { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Logout
|
||||||
|
{
|
||||||
|
public static class LogoutOptions
|
||||||
|
{
|
||||||
|
public static readonly bool ShowLogoutPrompt = true;
|
||||||
|
public static readonly bool AutomaticRedirectAfterSignOut = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Entities;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||||
|
{
|
||||||
|
public class ApiScopeSummaryModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiScopeModel : ApiScopeSummaryModel
|
||||||
|
{
|
||||||
|
public string? UserClaims { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApiScopeRepository
|
||||||
|
{
|
||||||
|
private readonly ConfigurationDbContext _context;
|
||||||
|
|
||||||
|
public ApiScopeRepository(ConfigurationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ApiScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||||
|
{
|
||||||
|
var query = _context.ApiScopes
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (!String.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = query.Select(x => new ApiScopeSummaryModel
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
DisplayName = x.DisplayName
|
||||||
|
});
|
||||||
|
|
||||||
|
return await result.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiScopeModel?> GetByIdAsync(string id)
|
||||||
|
{
|
||||||
|
var scope = await _context.ApiScopes
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == id);
|
||||||
|
|
||||||
|
if (scope == null) return null;
|
||||||
|
|
||||||
|
return new ApiScopeModel
|
||||||
|
{
|
||||||
|
Name = scope.Name,
|
||||||
|
DisplayName = scope.DisplayName,
|
||||||
|
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(ApiScopeModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var scope = new Duende.IdentityServer.Models.ApiScope()
|
||||||
|
{
|
||||||
|
Name = model.Name,
|
||||||
|
DisplayName = model.DisplayName?.Trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||||
|
if (claims.Any())
|
||||||
|
{
|
||||||
|
scope.UserClaims = claims.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||||
|
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||||
|
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||||
|
// generators
|
||||||
|
_context.ApiScopes.Add(scope.ToEntity());
|
||||||
|
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(ApiScopeModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var scope = await _context.ApiScopes
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||||
|
|
||||||
|
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||||
|
|
||||||
|
if (scope.DisplayName != model.DisplayName)
|
||||||
|
{
|
||||||
|
scope.DisplayName = model.DisplayName?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||||
|
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||||
|
|
||||||
|
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||||
|
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||||
|
|
||||||
|
if (claimsToRemove.Length != 0)
|
||||||
|
{
|
||||||
|
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||||
|
}
|
||||||
|
if (claimsToAdd.Length != 0)
|
||||||
|
{
|
||||||
|
scope.UserClaims.AddRange(claimsToAdd.Select(x => new ApiScopeClaim
|
||||||
|
{
|
||||||
|
Type = x,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string id)
|
||||||
|
{
|
||||||
|
var scope = await _context.ApiScopes.SingleOrDefaultAsync(x => x.Name == id);
|
||||||
|
|
||||||
|
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||||
|
|
||||||
|
_context.ApiScopes.Remove(scope);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.ApiScopes.EditModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="identityscopes-page">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Edit Identity Scope: @Model.InputModel.Name
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<partial name="_ValidationSummary" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||||
|
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||||
|
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class EditModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ApiScopeRepository _repository;
|
||||||
|
|
||||||
|
public EditModel(ApiScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public ApiScopeModel InputModel { get; set; } = default!;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public string Button { get; set; } = default!;
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync(string id)
|
||||||
|
{
|
||||||
|
var model = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InputModel = model;
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync(string id)
|
||||||
|
{
|
||||||
|
if (Button == "delete")
|
||||||
|
{
|
||||||
|
await _repository.DeleteAsync(id);
|
||||||
|
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await _repository.UpdateAsync(InputModel);
|
||||||
|
return RedirectToPage("/Admin/ApiScopes/Edit", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.ApiScopes.IndexModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="clients-page">
|
||||||
|
<div class="row">
|
||||||
|
<h2>
|
||||||
|
API Scopes
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||||
|
<a asp-page="/Admin/ApiScopes/New" class="btn btn-success">New</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Display Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach(var scope in Model.Scopes)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><a asp-page="/Admin/ApiScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||||
|
<td>@scope.DisplayName</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ApiScopeRepository _repository;
|
||||||
|
|
||||||
|
public IndexModel(ApiScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ApiScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||||
|
public string? Filter { get; set; }
|
||||||
|
|
||||||
|
public async Task OnGetAsync(string? filter)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
Scopes = await _repository.GetAllAsync(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.ApiScopes.NewModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="clients-page">
|
||||||
|
<h2>
|
||||||
|
New API Scope
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<partial name="_ValidationSummary" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.Name"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||||
|
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class NewModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ApiScopeRepository _repository;
|
||||||
|
|
||||||
|
public NewModel(ApiScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public ApiScopeModel InputModel { get; set; } = default!;
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync()
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await _repository.CreateAsync(InputModel);
|
||||||
|
return RedirectToPage("/Admin/ApiScopes/Edit", new { id = InputModel.Name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Entities;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||||
|
using Duende.IdentityServer.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.Clients
|
||||||
|
{
|
||||||
|
public class ClientSummaryModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string ClientId { get; set; } = default!;
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Flow Flow { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateClientModel : ClientSummaryModel
|
||||||
|
{
|
||||||
|
public string Secret { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClientModel : CreateClientModel, IValidatableObject
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string AllowedScopes { get; set; } = default!;
|
||||||
|
|
||||||
|
public string? RedirectUri { get; set; }
|
||||||
|
public string? InitiateLoginUri { get; set; }
|
||||||
|
public string? PostLogoutRedirectUri { get; set; }
|
||||||
|
public string? FrontChannelLogoutUri { get; set; }
|
||||||
|
public string? BackChannelLogoutUri { get; set; }
|
||||||
|
|
||||||
|
private static readonly string[] memberNames = new[] { "RedirectUri" };
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
var errors = new List<ValidationResult>();
|
||||||
|
|
||||||
|
if (Flow == Flow.CodeFlowWithPkce)
|
||||||
|
{
|
||||||
|
if (RedirectUri == null)
|
||||||
|
{
|
||||||
|
errors.Add(new ValidationResult("Redirect URI is required.", memberNames));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Flow
|
||||||
|
{
|
||||||
|
ClientCredentials,
|
||||||
|
CodeFlowWithPkce
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClientRepository
|
||||||
|
{
|
||||||
|
private readonly ConfigurationDbContext _context;
|
||||||
|
|
||||||
|
public ClientRepository(ConfigurationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ClientSummaryModel>> GetAllAsync(string? filter = null)
|
||||||
|
{
|
||||||
|
var grants = new[] { GrantType.AuthorizationCode, GrantType.ClientCredentials };
|
||||||
|
|
||||||
|
var query = _context.Clients
|
||||||
|
.Include(x => x.AllowedGrantTypes)
|
||||||
|
.Where(x => x.AllowedGrantTypes.Count == 1 && x.AllowedGrantTypes.Any(grant => grants.Contains(grant.GrantType)));
|
||||||
|
|
||||||
|
if (!String.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.ClientId.Contains(filter) || x.ClientName.Contains(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = query.Select(x => new ClientSummaryModel
|
||||||
|
{
|
||||||
|
ClientId = x.ClientId,
|
||||||
|
Name = x.ClientName,
|
||||||
|
Flow = x.AllowedGrantTypes.Select(x => x.GrantType).Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce
|
||||||
|
});
|
||||||
|
|
||||||
|
return await result.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ClientModel?> GetByIdAsync(string id)
|
||||||
|
{
|
||||||
|
var client = await _context.Clients
|
||||||
|
.Include(x => x.AllowedGrantTypes)
|
||||||
|
.Include(x => x.AllowedScopes)
|
||||||
|
.Include(x => x.RedirectUris)
|
||||||
|
.Include(x => x.PostLogoutRedirectUris)
|
||||||
|
.Where(x => x.ClientId == id)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
|
if (client == null) return null;
|
||||||
|
|
||||||
|
return new ClientModel
|
||||||
|
{
|
||||||
|
ClientId = client.ClientId,
|
||||||
|
Name = client.ClientName,
|
||||||
|
Flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||||
|
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce,
|
||||||
|
AllowedScopes = client.AllowedScopes.Count != 0 ? client.AllowedScopes.Select(x => x.Scope).Aggregate((a, b) => $"{a} {b}") : string.Empty,
|
||||||
|
RedirectUri = client.RedirectUris.Select(x => x.RedirectUri).SingleOrDefault(),
|
||||||
|
InitiateLoginUri = client.InitiateLoginUri,
|
||||||
|
PostLogoutRedirectUri = client.PostLogoutRedirectUris.Select(x => x.PostLogoutRedirectUri).SingleOrDefault(),
|
||||||
|
FrontChannelLogoutUri = client.FrontChannelLogoutUri,
|
||||||
|
BackChannelLogoutUri = client.BackChannelLogoutUri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(CreateClientModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var client = new Duende.IdentityServer.Models.Client();
|
||||||
|
client.ClientId = model.ClientId.Trim();
|
||||||
|
client.ClientName = model.Name?.Trim();
|
||||||
|
|
||||||
|
client.ClientSecrets.Add(new Duende.IdentityServer.Models.Secret(model.Secret.Sha256()));
|
||||||
|
|
||||||
|
if (model.Flow == Flow.ClientCredentials)
|
||||||
|
{
|
||||||
|
client.AllowedGrantTypes = GrantTypes.ClientCredentials;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
client.AllowedGrantTypes = GrantTypes.Code;
|
||||||
|
client.AllowOfflineAccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||||
|
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||||
|
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||||
|
// generators
|
||||||
|
_context.Clients.Add(client.ToEntity());
|
||||||
|
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(ClientModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var client = await _context.Clients
|
||||||
|
.Include(x => x.AllowedGrantTypes)
|
||||||
|
.Include(x => x.AllowedScopes)
|
||||||
|
.Include(x => x.RedirectUris)
|
||||||
|
.Include(x => x.PostLogoutRedirectUris)
|
||||||
|
.SingleOrDefaultAsync(x => x.ClientId == model.ClientId);
|
||||||
|
|
||||||
|
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||||
|
|
||||||
|
if (client.ClientName != model.Name)
|
||||||
|
{
|
||||||
|
client.ClientName = model.Name?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes = model.AllowedScopes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||||
|
var currentScopes = (client.AllowedScopes.Select(x => x.Scope) ?? Enumerable.Empty<String>()).ToArray();
|
||||||
|
|
||||||
|
var scopesToAdd = scopes.Except(currentScopes).ToArray();
|
||||||
|
var scopesToRemove = currentScopes.Except(scopes).ToArray();
|
||||||
|
|
||||||
|
if (scopesToRemove.Length != 0)
|
||||||
|
{
|
||||||
|
client.AllowedScopes.RemoveAll(x => scopesToRemove.Contains(x.Scope));
|
||||||
|
}
|
||||||
|
if (scopesToAdd.Length != 0)
|
||||||
|
{
|
||||||
|
client.AllowedScopes.AddRange(scopesToAdd.Select(x => new ClientScope
|
||||||
|
{
|
||||||
|
Scope = x,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
var flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||||
|
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce;
|
||||||
|
|
||||||
|
if (flow == Flow.CodeFlowWithPkce)
|
||||||
|
{
|
||||||
|
if (client.RedirectUris.SingleOrDefault()?.RedirectUri != model.RedirectUri)
|
||||||
|
{
|
||||||
|
client.RedirectUris.Clear();
|
||||||
|
if (model.RedirectUri != null)
|
||||||
|
{
|
||||||
|
client.RedirectUris.Add(new ClientRedirectUri { RedirectUri = model.RedirectUri.Trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (client.InitiateLoginUri != model.InitiateLoginUri)
|
||||||
|
{
|
||||||
|
client.InitiateLoginUri = model.InitiateLoginUri;
|
||||||
|
}
|
||||||
|
if (client.PostLogoutRedirectUris.SingleOrDefault()?.PostLogoutRedirectUri != model.PostLogoutRedirectUri)
|
||||||
|
{
|
||||||
|
client.PostLogoutRedirectUris.Clear();
|
||||||
|
if (model.PostLogoutRedirectUri != null)
|
||||||
|
{
|
||||||
|
client.PostLogoutRedirectUris.Add(new ClientPostLogoutRedirectUri { PostLogoutRedirectUri = model.PostLogoutRedirectUri.Trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (client.FrontChannelLogoutUri != model.FrontChannelLogoutUri)
|
||||||
|
{
|
||||||
|
client.FrontChannelLogoutUri = model.FrontChannelLogoutUri?.Trim();
|
||||||
|
}
|
||||||
|
if (client.BackChannelLogoutUri != model.BackChannelLogoutUri)
|
||||||
|
{
|
||||||
|
client.BackChannelLogoutUri = model.BackChannelLogoutUri?.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string clientId)
|
||||||
|
{
|
||||||
|
var client = await _context.Clients.SingleOrDefaultAsync(x => x.ClientId == clientId);
|
||||||
|
|
||||||
|
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||||
|
|
||||||
|
_context.Clients.Remove(client);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.Clients.IndexModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="clients-page">
|
||||||
|
<div class="row">
|
||||||
|
<h2>
|
||||||
|
Clients
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||||
|
<a asp-page="/Admin/Clients/New" class="btn btn-success">New</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Client ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Flow</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach(var client in Model.Clients)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><a asp-page="/Admin/Clients/Edit" asp-route-id="@client.ClientId">@client.ClientId</a></td>
|
||||||
|
<td>@client.Name</td>
|
||||||
|
<td>@client.Flow</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.Clients
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ClientRepository _repository;
|
||||||
|
|
||||||
|
public IndexModel(ClientRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ClientSummaryModel> Clients { get; private set; } = default!;
|
||||||
|
public string? Filter { get; set; }
|
||||||
|
|
||||||
|
public async Task OnGetAsync(string? filter)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
Clients = await _repository.GetAllAsync(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using IdentityModel;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.Clients
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class NewModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ClientRepository _repository;
|
||||||
|
|
||||||
|
public NewModel(ClientRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public CreateClientModel InputModel { get; set; } = default!;
|
||||||
|
|
||||||
|
public bool Created { get; set; }
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
InputModel = new CreateClientModel
|
||||||
|
{
|
||||||
|
Secret = Convert.ToBase64String(CryptoRandom.CreateRandomKey(16))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync()
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await _repository.CreateAsync(InputModel);
|
||||||
|
Created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.IdentityScopes.EditModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="identityscopes-page">
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Edit Identity Scope: @Model.InputModel.Name
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<partial name="_ValidationSummary" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||||
|
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||||
|
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class EditModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly IdentityScopeRepository _repository;
|
||||||
|
|
||||||
|
public EditModel(IdentityScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public string? Button { get; set; }
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnGetAsync(string id)
|
||||||
|
{
|
||||||
|
var model = await _repository.GetByIdAsync(id);
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InputModel = model;
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync(string id)
|
||||||
|
{
|
||||||
|
if (Button == "delete")
|
||||||
|
{
|
||||||
|
await _repository.DeleteAsync(id);
|
||||||
|
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await _repository.UpdateAsync(InputModel);
|
||||||
|
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Entities;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||||
|
{
|
||||||
|
public class IdentityScopeSummaryModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IdentityScopeModel : IdentityScopeSummaryModel
|
||||||
|
{
|
||||||
|
public string? UserClaims { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IdentityScopeRepository
|
||||||
|
{
|
||||||
|
private readonly ConfigurationDbContext _context;
|
||||||
|
|
||||||
|
public IdentityScopeRepository(ConfigurationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<IdentityScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||||
|
{
|
||||||
|
var query = _context.IdentityResources
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (!String.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = query.Select(x => new IdentityScopeSummaryModel
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
DisplayName = x.DisplayName
|
||||||
|
});
|
||||||
|
|
||||||
|
return await result.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityScopeModel?> GetByIdAsync(string id)
|
||||||
|
{
|
||||||
|
var scope = await _context.IdentityResources
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == id);
|
||||||
|
|
||||||
|
if (scope == null) return null;
|
||||||
|
|
||||||
|
return new IdentityScopeModel
|
||||||
|
{
|
||||||
|
Name = scope.Name,
|
||||||
|
DisplayName = scope.DisplayName,
|
||||||
|
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(IdentityScopeModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var scope = new Duende.IdentityServer.Models.IdentityResource()
|
||||||
|
{
|
||||||
|
Name = model.Name,
|
||||||
|
DisplayName = model.DisplayName?.Trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||||
|
if (claims.Any())
|
||||||
|
{
|
||||||
|
scope.UserClaims = claims.ToList();
|
||||||
|
}
|
||||||
|
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||||
|
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||||
|
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||||
|
// generators
|
||||||
|
_context.IdentityResources.Add(scope.ToEntity());
|
||||||
|
#pragma warning restore CA1849
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(IdentityScopeModel model)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(model);
|
||||||
|
var scope = await _context.IdentityResources
|
||||||
|
.Include(x => x.UserClaims)
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||||
|
|
||||||
|
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||||
|
|
||||||
|
if (scope.DisplayName != model.DisplayName)
|
||||||
|
{
|
||||||
|
scope.DisplayName = model.DisplayName?.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||||
|
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||||
|
|
||||||
|
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||||
|
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||||
|
|
||||||
|
if (claimsToRemove.Length != 0)
|
||||||
|
{
|
||||||
|
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||||
|
}
|
||||||
|
if (claimsToAdd.Length != 0)
|
||||||
|
{
|
||||||
|
scope.UserClaims.AddRange(claimsToAdd.Select(x => new IdentityResourceClaim
|
||||||
|
{
|
||||||
|
Type = x,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string id)
|
||||||
|
{
|
||||||
|
var scope = await _context.IdentityResources.SingleOrDefaultAsync(x => x.Name == id);
|
||||||
|
|
||||||
|
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||||
|
|
||||||
|
_context.IdentityResources.Remove(scope);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.IdentityScopes.IndexModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="clients-page">
|
||||||
|
<div class="row">
|
||||||
|
<h2>
|
||||||
|
Identity Scopes
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||||
|
<a asp-page="/Admin/IdentityScopes/New" class="btn btn-success">New</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Display Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach(var scope in Model.Scopes)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><a asp-page="/Admin/IdentityScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||||
|
<td>@scope.DisplayName</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class IndexModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly IdentityScopeRepository _repository;
|
||||||
|
|
||||||
|
public IndexModel(IdentityScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IdentityScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||||
|
public string? Filter { get; set; }
|
||||||
|
|
||||||
|
public async Task OnGetAsync(string? filter)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
Scopes = await _repository.GetAllAsync(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@page
|
||||||
|
@model Microser.IdS.Pages.Admin.IdentityScopes.NewModel
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="clients-page">
|
||||||
|
<h2>
|
||||||
|
New Identity Scope
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<partial name="_ValidationSummary" />
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.Name"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||||
|
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||||
|
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) Duende Software. All rights reserved.
|
||||||
|
// See LICENSE in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||||
|
{
|
||||||
|
[SecurityHeaders]
|
||||||
|
[Authorize]
|
||||||
|
public class NewModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly IdentityScopeRepository _repository;
|
||||||
|
|
||||||
|
public NewModel(IdentityScopeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||||
|
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAsync()
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
await _repository.CreateAsync(InputModel);
|
||||||
|
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id = InputModel.Name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Page();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user