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();
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.API.Weather/Data.cs
Normal file
35
src/Microser/Microser.API.Weather/Data.cs
Normal file
@@ -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>
|
||||
36
src/Microser/Microser.API.Weather/Microser.API.Weather.http
Normal file
36
src/Microser/Microser.API.Weather/Microser.API.Weather.http
Normal file
@@ -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
|
||||
|
||||
###
|
||||
50
src/Microser/Microser.API.Weather/Program.cs
Normal file
50
src/Microser/Microser.API.Weather/Program.cs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Microser/Microser.API.Weather/appsettings.json
Normal file
9
src/Microser/Microser.API.Weather/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
20
src/Microser/Microser.BlazorAppClient/Components/App.razor
Normal file
20
src/Microser/Microser.BlazorAppClient/Components/App.razor
Normal file
@@ -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>
|
||||
191
src/Microser/Microser.BlazorAppClient/Program.cs
Normal file
191
src/Microser/Microser.BlazorAppClient/Program.cs
Normal file
@@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
83
src/Microser/Microser.BlazorAppClient/ProjectAccesses.cs
Normal file
83
src/Microser/Microser.BlazorAppClient/ProjectAccesses.cs
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Microser/Microser.BlazorAppClient/appsettings.json
Normal file
9
src/Microser/Microser.BlazorAppClient/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
51
src/Microser/Microser.BlazorAppClient/wwwroot/app.css
Normal file
51
src/Microser/Microser.BlazorAppClient/wwwroot/app.css
Normal file
@@ -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;
|
||||
}
|
||||
7
src/Microser/Microser.BlazorAppClient/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
src/Microser/Microser.BlazorAppClient/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
src/Microser/Microser.BlazorAppClient/wwwroot/favicon.png
Normal file
BIN
src/Microser/Microser.BlazorAppClient/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
9
src/Microser/Microser.Core/Microser.Core.csproj
Normal file
9
src/Microser/Microser.Core/Microser.Core.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
14
src/Microser/Microser.Core/Models/WeatherForecast.cs
Normal file
14
src/Microser/Microser.Core/Models/WeatherForecast.cs
Normal file
@@ -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; }
|
||||
}
|
||||
88
src/Microser/Microser.IdS/Config.cs
Normal file
88
src/Microser/Microser.IdS/Config.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Microser.IdS
|
||||
{
|
||||
public static class Config
|
||||
{
|
||||
public static IEnumerable<IdentityResource> IdentityResources =>
|
||||
new IdentityResource[]
|
||||
{
|
||||
new IdentityResources.OpenId(),
|
||||
new IdentityResources.Profile(),
|
||||
new IdentityResources.Address(),
|
||||
new IdentityResources.Email(),
|
||||
new IdentityResource(
|
||||
"roles",
|
||||
"Your role(s)",
|
||||
new List<string>(){ JwtClaimTypes.Role })
|
||||
};
|
||||
|
||||
public static IEnumerable<ApiScope> ApiScopes =>
|
||||
new ApiScope[]
|
||||
{
|
||||
new ApiScope("scope1"),
|
||||
new ApiScope("scope2"),
|
||||
|
||||
new ApiScope("microser_api_weather"),
|
||||
};
|
||||
|
||||
public static IEnumerable<Client> Clients =>
|
||||
new Client[]
|
||||
{
|
||||
new Client
|
||||
{
|
||||
ClientId = "dotnet_blazor_serverapp",
|
||||
ClientName = "Blazor Server App",
|
||||
ClientSecrets = {
|
||||
new Secret("E8C65E41BB0E4E519D409023CF5112F4".Sha256())
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.Code,
|
||||
RequirePkce = true,
|
||||
RequireClientSecret = true,
|
||||
AllowedCorsOrigins = { "https://localhost:7001" },
|
||||
AllowedScopes = {
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServerConstants.StandardScopes.Profile,
|
||||
IdentityServerConstants.StandardScopes.Address,
|
||||
IdentityServerConstants.StandardScopes.Email,
|
||||
"roles",
|
||||
"scope1",
|
||||
"microser_api_weather"
|
||||
},
|
||||
RedirectUris = { "https://localhost:7001/signin-oidc" },
|
||||
PostLogoutRedirectUris = { "https://localhost:7001/signout-callback-oidc" },
|
||||
Enabled = true
|
||||
},
|
||||
|
||||
// m2m client credentials flow client
|
||||
new Client
|
||||
{
|
||||
ClientId = "m2m.client",
|
||||
ClientName = "Client Credentials Client",
|
||||
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
|
||||
|
||||
AllowedScopes = { "scope1" }
|
||||
},
|
||||
|
||||
// interactive client using code flow + pkce
|
||||
new Client
|
||||
{
|
||||
ClientId = "interactive",
|
||||
ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
|
||||
|
||||
AllowedGrantTypes = GrantTypes.Code,
|
||||
|
||||
RedirectUris = { "https://localhost:44300/signin-oidc" },
|
||||
FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
|
||||
PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" },
|
||||
|
||||
AllowOfflineAccess = true,
|
||||
AllowedScopes = { "openid", "profile", "scope2" }
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
106
src/Microser/Microser.IdS/HostingExtensions.cs
Normal file
106
src/Microser/Microser.IdS/HostingExtensions.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Duende.IdentityServer;
|
||||
using Microser.IdS.Pages.Admin.ApiScopes;
|
||||
using Microser.IdS.Pages.Admin.Clients;
|
||||
using Microser.IdS.Pages.Admin.IdentityScopes;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
|
||||
namespace Microser.IdS
|
||||
{
|
||||
internal static class HostingExtensions
|
||||
{
|
||||
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||
|
||||
var isBuilder = builder.Services
|
||||
.AddIdentityServer(options =>
|
||||
{
|
||||
options.Events.RaiseErrorEvents = true;
|
||||
options.Events.RaiseInformationEvents = true;
|
||||
options.Events.RaiseFailureEvents = true;
|
||||
options.Events.RaiseSuccessEvents = true;
|
||||
|
||||
// see https://docs.duendesoftware.com/identityserver/v5/fundamentals/resources/
|
||||
options.EmitStaticAudienceClaim = true;
|
||||
})
|
||||
.AddTestUsers(TestUsers.Users)
|
||||
// this adds the config data from DB (clients, resources, CORS)
|
||||
.AddConfigurationStore(options =>
|
||||
{
|
||||
options.ConfigureDbContext = b =>
|
||||
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
|
||||
})
|
||||
// this is something you will want in production to reduce load on and requests to the DB
|
||||
//.AddConfigurationStoreCache()
|
||||
//
|
||||
// this adds the operational data from DB (codes, tokens, consents)
|
||||
.AddOperationalStore(options =>
|
||||
{
|
||||
options.ConfigureDbContext = b =>
|
||||
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication()
|
||||
.AddGoogle(options =>
|
||||
{
|
||||
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
|
||||
|
||||
// register your IdentityServer with Google at https://console.developers.google.com
|
||||
// enable the Google+ API
|
||||
// set the redirect URI to https://localhost:5001/signin-google
|
||||
options.ClientId = "copy client ID from Google here";
|
||||
options.ClientSecret = "copy client secret from Google here";
|
||||
});
|
||||
|
||||
// this adds the necessary config for the simple admin/config pages
|
||||
{
|
||||
builder.Services.AddAuthorization(options =>
|
||||
options.AddPolicy("admin",
|
||||
policy => policy.RequireClaim("sub", "1"))
|
||||
);
|
||||
|
||||
builder.Services.Configure<RazorPagesOptions>(options =>
|
||||
options.Conventions.AuthorizeFolder("/Admin", "admin"));
|
||||
|
||||
builder.Services.AddTransient<Microser.IdS.Pages.Portal.ClientRepository>();
|
||||
builder.Services.AddTransient<ClientRepository>();
|
||||
builder.Services.AddTransient<IdentityScopeRepository>();
|
||||
builder.Services.AddTransient<ApiScopeRepository>();
|
||||
}
|
||||
|
||||
// if you want to use server-side sessions: https://blog.duendesoftware.com/posts/20220406_session_management/
|
||||
// then enable it
|
||||
//isBuilder.AddServerSideSessions();
|
||||
//
|
||||
// and put some authorization on the admin/management pages using the same policy created above
|
||||
//builder.Services.Configure<RazorPagesOptions>(options =>
|
||||
// options.Conventions.AuthorizeFolder("/ServerSideSessions", "admin"));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static WebApplication ConfigurePipeline(this WebApplication app)
|
||||
{
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseIdentityServer();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages()
|
||||
.RequireAuthorization();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/Microser/Microser.IdS/IdentityServer.db
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db
Normal file
Binary file not shown.
BIN
src/Microser/Microser.IdS/IdentityServer.db-shm
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db-shm
Normal file
Binary file not shown.
BIN
src/Microser/Microser.IdS/IdentityServer.db-wal
Normal file
BIN
src/Microser/Microser.IdS/IdentityServer.db-wal
Normal file
Binary file not shown.
24
src/Microser/Microser.IdS/Microser.IdS.csproj
Normal file
24
src/Microser/Microser.IdS/Microser.IdS.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.0.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
301
src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql
Normal file
301
src/Microser/Microser.IdS/Migrations/ConfigurationDb.sql
Normal file
@@ -0,0 +1,301 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "ApiResources" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResources" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"AllowedAccessTokenSigningAlgorithms" TEXT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"RequireResourceIndicator" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Required" INTEGER NOT NULL,
|
||||
"Emphasize" INTEGER NOT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Clients" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_Clients" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"ProtocolType" TEXT NOT NULL,
|
||||
"RequireClientSecret" INTEGER NOT NULL,
|
||||
"ClientName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"ClientUri" TEXT NULL,
|
||||
"LogoUri" TEXT NULL,
|
||||
"RequireConsent" INTEGER NOT NULL,
|
||||
"AllowRememberConsent" INTEGER NOT NULL,
|
||||
"AlwaysIncludeUserClaimsInIdToken" INTEGER NOT NULL,
|
||||
"RequirePkce" INTEGER NOT NULL,
|
||||
"AllowPlainTextPkce" INTEGER NOT NULL,
|
||||
"RequireRequestObject" INTEGER NOT NULL,
|
||||
"AllowAccessTokensViaBrowser" INTEGER NOT NULL,
|
||||
"RequireDPoP" INTEGER NOT NULL,
|
||||
"DPoPValidationMode" INTEGER NOT NULL,
|
||||
"DPoPClockSkew" TEXT NOT NULL,
|
||||
"FrontChannelLogoutUri" TEXT NULL,
|
||||
"FrontChannelLogoutSessionRequired" INTEGER NOT NULL,
|
||||
"BackChannelLogoutUri" TEXT NULL,
|
||||
"BackChannelLogoutSessionRequired" INTEGER NOT NULL,
|
||||
"AllowOfflineAccess" INTEGER NOT NULL,
|
||||
"IdentityTokenLifetime" INTEGER NOT NULL,
|
||||
"AllowedIdentityTokenSigningAlgorithms" TEXT NULL,
|
||||
"AccessTokenLifetime" INTEGER NOT NULL,
|
||||
"AuthorizationCodeLifetime" INTEGER NOT NULL,
|
||||
"ConsentLifetime" INTEGER NULL,
|
||||
"AbsoluteRefreshTokenLifetime" INTEGER NOT NULL,
|
||||
"SlidingRefreshTokenLifetime" INTEGER NOT NULL,
|
||||
"RefreshTokenUsage" INTEGER NOT NULL,
|
||||
"UpdateAccessTokenClaimsOnRefresh" INTEGER NOT NULL,
|
||||
"RefreshTokenExpiration" INTEGER NOT NULL,
|
||||
"AccessTokenType" INTEGER NOT NULL,
|
||||
"EnableLocalLogin" INTEGER NOT NULL,
|
||||
"IncludeJwtId" INTEGER NOT NULL,
|
||||
"AlwaysSendClientClaims" INTEGER NOT NULL,
|
||||
"ClientClaimsPrefix" TEXT NULL,
|
||||
"PairWiseSubjectSalt" TEXT NULL,
|
||||
"InitiateLoginUri" TEXT NULL,
|
||||
"UserSsoLifetime" INTEGER NULL,
|
||||
"UserCodeType" TEXT NULL,
|
||||
"DeviceCodeLifetime" INTEGER NOT NULL,
|
||||
"CibaLifetime" INTEGER NULL,
|
||||
"PollingInterval" INTEGER NULL,
|
||||
"CoordinateLifetimeWithUserSession" INTEGER NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL,
|
||||
"PushedAuthorizationLifetime" INTEGER NULL,
|
||||
"RequirePushedAuthorization" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityProviders" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityProviders" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scheme" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Properties" TEXT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"LastAccessed" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResources" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResources" PRIMARY KEY AUTOINCREMENT,
|
||||
"Enabled" INTEGER NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Required" INTEGER NOT NULL,
|
||||
"Emphasize" INTEGER NOT NULL,
|
||||
"ShowInDiscoveryDocument" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Updated" TEXT NULL,
|
||||
"NonEditable" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceClaims_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceProperties_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scope" TEXT NOT NULL,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceScopes_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiResourceSecrets" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceSecrets" PRIMARY KEY AUTOINCREMENT,
|
||||
"ApiResourceId" INTEGER NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiResourceSecrets_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopeClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"ScopeId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiScopeClaims_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ApiScopeProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ScopeId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ApiScopeProperties_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientClaims_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientCorsOrigins" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientCorsOrigins" PRIMARY KEY AUTOINCREMENT,
|
||||
"Origin" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientCorsOrigins_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientGrantTypes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientGrantTypes" PRIMARY KEY AUTOINCREMENT,
|
||||
"GrantType" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientGrantTypes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientIdPRestrictions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientIdPRestrictions" PRIMARY KEY AUTOINCREMENT,
|
||||
"Provider" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientIdPRestrictions_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientPostLogoutRedirectUris" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientPostLogoutRedirectUris" PRIMARY KEY AUTOINCREMENT,
|
||||
"PostLogoutRedirectUri" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientPostLogoutRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ClientProperties_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientRedirectUris" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientRedirectUris" PRIMARY KEY AUTOINCREMENT,
|
||||
"RedirectUri" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientScopes" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientScopes" PRIMARY KEY AUTOINCREMENT,
|
||||
"Scope" TEXT NOT NULL,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
CONSTRAINT "FK_ClientScopes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "ClientSecrets" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientSecrets" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClientId" INTEGER NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_ClientSecrets_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResourceClaims" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceClaims" PRIMARY KEY AUTOINCREMENT,
|
||||
"IdentityResourceId" INTEGER NOT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "IdentityResourceProperties" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceProperties" PRIMARY KEY AUTOINCREMENT,
|
||||
"IdentityResourceId" INTEGER NOT NULL,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Value" TEXT NOT NULL,
|
||||
CONSTRAINT "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceClaims_ApiResourceId_Type" ON "ApiResourceClaims" ("ApiResourceId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceProperties_ApiResourceId_Key" ON "ApiResourceProperties" ("ApiResourceId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResources_Name" ON "ApiResources" ("Name");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiResourceScopes_ApiResourceId_Scope" ON "ApiResourceScopes" ("ApiResourceId", "Scope");
|
||||
|
||||
CREATE INDEX "IX_ApiResourceSecrets_ApiResourceId" ON "ApiResourceSecrets" ("ApiResourceId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopeClaims_ScopeId_Type" ON "ApiScopeClaims" ("ScopeId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopeProperties_ScopeId_Key" ON "ApiScopeProperties" ("ScopeId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ApiScopes_Name" ON "ApiScopes" ("Name");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientClaims_ClientId_Type_Value" ON "ClientClaims" ("ClientId", "Type", "Value");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientCorsOrigins_ClientId_Origin" ON "ClientCorsOrigins" ("ClientId", "Origin");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientGrantTypes_ClientId_GrantType" ON "ClientGrantTypes" ("ClientId", "GrantType");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientIdPRestrictions_ClientId_Provider" ON "ClientIdPRestrictions" ("ClientId", "Provider");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri" ON "ClientPostLogoutRedirectUris" ("ClientId", "PostLogoutRedirectUri");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientProperties_ClientId_Key" ON "ClientProperties" ("ClientId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientRedirectUris_ClientId_RedirectUri" ON "ClientRedirectUris" ("ClientId", "RedirectUri");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_Clients_ClientId" ON "Clients" ("ClientId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ClientScopes_ClientId_Scope" ON "ClientScopes" ("ClientId", "Scope");
|
||||
|
||||
CREATE INDEX "IX_ClientSecrets_ClientId" ON "ClientSecrets" ("ClientId");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityProviders_Scheme" ON "IdentityProviders" ("Scheme");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResourceClaims_IdentityResourceId_Type" ON "IdentityResourceClaims" ("IdentityResourceId", "Type");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResourceProperties_IdentityResourceId_Key" ON "IdentityResourceProperties" ("IdentityResourceId", "Key");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_IdentityResources_Name" ON "IdentityResources" ("Name");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240312131641_Configuration', '8.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
1067
src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs
generated
Normal file
1067
src/Microser/Microser.IdS/Migrations/ConfigurationDb/20240312131641_Configuration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,721 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.ConfigurationDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Configuration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResources",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireResourceIndicator = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResources", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Clients",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ProtocolType = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
RequireClientSecret = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
ClientUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
LogoUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
RequireConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowRememberConsent = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequirePkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowPlainTextPkce = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireRequestObject = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowAccessTokensViaBrowser = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RequireDPoP = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DPoPValidationMode = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
DPoPClockSkew = table.Column<TimeSpan>(type: "TEXT", nullable: false),
|
||||
FrontChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
BackChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
BackChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AllowOfflineAccess = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IdentityTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
AccessTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AuthorizationCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConsentLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
SlidingRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
RefreshTokenUsage = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
RefreshTokenExpiration = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AccessTokenType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
EnableLocalLogin = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IncludeJwtId = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
AlwaysSendClientClaims = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ClientClaimsPrefix = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
PairWiseSubjectSalt = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
InitiateLoginUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
UserSsoLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
UserCodeType = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
DeviceCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CibaLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
PollingInterval = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
CoordinateLifetimeWithUserSession = table.Column<bool>(type: "INTEGER", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
PushedAuthorizationLifetime = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
RequirePushedAuthorization = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Clients", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityProviders",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scheme = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
Properties = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityProviders", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResources",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Required = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResources", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiResourceSecrets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
|
||||
column: x => x.ApiResourceId,
|
||||
principalTable: "ApiResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopeClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
|
||||
column: x => x.ScopeId,
|
||||
principalTable: "ApiScopes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiScopeProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
|
||||
column: x => x.ScopeId,
|
||||
principalTable: "ApiScopes",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientClaims_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientCorsOrigins",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Origin = table.Column<string>(type: "TEXT", maxLength: 150, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientCorsOrigins_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientGrantTypes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
GrantType = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientGrantTypes_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientIdPRestrictions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Provider = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientIdPRestrictions_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientPostLogoutRedirectUris",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
PostLogoutRedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientProperties_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientRedirectUris",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
RedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientRedirectUris_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientScopes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientScopes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientScopes_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientSecrets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientSecrets", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ClientSecrets_Clients_ClientId",
|
||||
column: x => x.ClientId,
|
||||
principalTable: "Clients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResourceClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
|
||||
column: x => x.IdentityResourceId,
|
||||
principalTable: "IdentityResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "IdentityResourceProperties",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId",
|
||||
column: x => x.IdentityResourceId,
|
||||
principalTable: "IdentityResources",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceClaims_ApiResourceId_Type",
|
||||
table: "ApiResourceClaims",
|
||||
columns: new[] { "ApiResourceId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceProperties_ApiResourceId_Key",
|
||||
table: "ApiResourceProperties",
|
||||
columns: new[] { "ApiResourceId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResources_Name",
|
||||
table: "ApiResources",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceScopes_ApiResourceId_Scope",
|
||||
table: "ApiResourceScopes",
|
||||
columns: new[] { "ApiResourceId", "Scope" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiResourceSecrets_ApiResourceId",
|
||||
table: "ApiResourceSecrets",
|
||||
column: "ApiResourceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopeClaims_ScopeId_Type",
|
||||
table: "ApiScopeClaims",
|
||||
columns: new[] { "ScopeId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopeProperties_ScopeId_Key",
|
||||
table: "ApiScopeProperties",
|
||||
columns: new[] { "ScopeId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiScopes_Name",
|
||||
table: "ApiScopes",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientClaims_ClientId_Type_Value",
|
||||
table: "ClientClaims",
|
||||
columns: new[] { "ClientId", "Type", "Value" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientCorsOrigins_ClientId_Origin",
|
||||
table: "ClientCorsOrigins",
|
||||
columns: new[] { "ClientId", "Origin" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientGrantTypes_ClientId_GrantType",
|
||||
table: "ClientGrantTypes",
|
||||
columns: new[] { "ClientId", "GrantType" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientIdPRestrictions_ClientId_Provider",
|
||||
table: "ClientIdPRestrictions",
|
||||
columns: new[] { "ClientId", "Provider" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri",
|
||||
table: "ClientPostLogoutRedirectUris",
|
||||
columns: new[] { "ClientId", "PostLogoutRedirectUri" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientProperties_ClientId_Key",
|
||||
table: "ClientProperties",
|
||||
columns: new[] { "ClientId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientRedirectUris_ClientId_RedirectUri",
|
||||
table: "ClientRedirectUris",
|
||||
columns: new[] { "ClientId", "RedirectUri" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Clients_ClientId",
|
||||
table: "Clients",
|
||||
column: "ClientId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientScopes_ClientId_Scope",
|
||||
table: "ClientScopes",
|
||||
columns: new[] { "ClientId", "Scope" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ClientSecrets_ClientId",
|
||||
table: "ClientSecrets",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityProviders_Scheme",
|
||||
table: "IdentityProviders",
|
||||
column: "Scheme",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResourceClaims_IdentityResourceId_Type",
|
||||
table: "IdentityResourceClaims",
|
||||
columns: new[] { "IdentityResourceId", "Type" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResourceProperties_IdentityResourceId_Key",
|
||||
table: "IdentityResourceProperties",
|
||||
columns: new[] { "IdentityResourceId", "Key" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_IdentityResources_Name",
|
||||
table: "IdentityResources",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResourceSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopeClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopeProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientCorsOrigins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientGrantTypes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientIdPRestrictions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientPostLogoutRedirectUris");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientRedirectUris");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityProviders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResourceClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResourceProperties");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiResources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiScopes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Clients");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "IdentityResources");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
99
src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql
Normal file
99
src/Microser/Microser.IdS/Migrations/PersistedGrantDb.sql
Normal file
@@ -0,0 +1,99 @@
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "DeviceCodes" (
|
||||
"UserCode" TEXT NOT NULL CONSTRAINT "PK_DeviceCodes" PRIMARY KEY,
|
||||
"DeviceCode" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"CreationTime" TEXT NOT NULL,
|
||||
"Expiration" TEXT NOT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Keys" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Keys" PRIMARY KEY,
|
||||
"Version" INTEGER NOT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Use" TEXT NULL,
|
||||
"Algorithm" TEXT NOT NULL,
|
||||
"IsX509Certificate" INTEGER NOT NULL,
|
||||
"DataProtected" INTEGER NOT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "PersistedGrants" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_PersistedGrants" PRIMARY KEY AUTOINCREMENT,
|
||||
"Key" TEXT NULL,
|
||||
"Type" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"ClientId" TEXT NOT NULL,
|
||||
"Description" TEXT NULL,
|
||||
"CreationTime" TEXT NOT NULL,
|
||||
"Expiration" TEXT NULL,
|
||||
"ConsumedTime" TEXT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "PushedAuthorizationRequests" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_PushedAuthorizationRequests" PRIMARY KEY AUTOINCREMENT,
|
||||
"ReferenceValueHash" TEXT NOT NULL,
|
||||
"ExpiresAtUtc" TEXT NOT NULL,
|
||||
"Parameters" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "ServerSideSessions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ServerSideSessions" PRIMARY KEY AUTOINCREMENT,
|
||||
"Key" TEXT NOT NULL,
|
||||
"Scheme" TEXT NOT NULL,
|
||||
"SubjectId" TEXT NOT NULL,
|
||||
"SessionId" TEXT NULL,
|
||||
"DisplayName" TEXT NULL,
|
||||
"Created" TEXT NOT NULL,
|
||||
"Renewed" TEXT NOT NULL,
|
||||
"Expires" TEXT NULL,
|
||||
"Data" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_DeviceCodes_DeviceCode" ON "DeviceCodes" ("DeviceCode");
|
||||
|
||||
CREATE INDEX "IX_DeviceCodes_Expiration" ON "DeviceCodes" ("Expiration");
|
||||
|
||||
CREATE INDEX "IX_Keys_Use" ON "Keys" ("Use");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_ConsumedTime" ON "PersistedGrants" ("ConsumedTime");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_Expiration" ON "PersistedGrants" ("Expiration");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_PersistedGrants_Key" ON "PersistedGrants" ("Key");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_SubjectId_ClientId_Type" ON "PersistedGrants" ("SubjectId", "ClientId", "Type");
|
||||
|
||||
CREATE INDEX "IX_PersistedGrants_SubjectId_SessionId_Type" ON "PersistedGrants" ("SubjectId", "SessionId", "Type");
|
||||
|
||||
CREATE INDEX "IX_PushedAuthorizationRequests_ExpiresAtUtc" ON "PushedAuthorizationRequests" ("ExpiresAtUtc");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_PushedAuthorizationRequests_ReferenceValueHash" ON "PushedAuthorizationRequests" ("ReferenceValueHash");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_DisplayName" ON "ServerSideSessions" ("DisplayName");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_Expires" ON "ServerSideSessions" ("Expires");
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ServerSideSessions_Key" ON "ServerSideSessions" ("Key");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_SessionId" ON "ServerSideSessions" ("SessionId");
|
||||
|
||||
CREATE INDEX "IX_ServerSideSessions_SubjectId" ON "ServerSideSessions" ("SubjectId");
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20240312131625_Grants', '8.0.0');
|
||||
|
||||
COMMIT;
|
||||
|
||||
259
src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs
generated
Normal file
259
src/Microser/Microser.IdS/Migrations/PersistedGrantDb/20240312131625_Grants.Designer.cs
generated
Normal file
@@ -0,0 +1,259 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
[DbContext(typeof(PersistedGrantDbContext))]
|
||||
[Migration("20240312131625_Grants")]
|
||||
partial class Grants
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
b.Property<string>("UserCode")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserCode");
|
||||
|
||||
b.HasIndex("DeviceCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.ToTable("DeviceCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Algorithm")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DataProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsX509Certificate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Use")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Use");
|
||||
|
||||
b.ToTable("Keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ConsumedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ConsumedTime");
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||
|
||||
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||
|
||||
b.ToTable("PersistedGrants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceValueHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAtUtc");
|
||||
|
||||
b.HasIndex("ReferenceValueHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PushedAuthorizationRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Renewed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scheme")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayName");
|
||||
|
||||
b.HasIndex("Expires");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.ToTable("ServerSideSessions", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Grants : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DeviceCodes",
|
||||
columns: table => new
|
||||
{
|
||||
UserCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
DeviceCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Keys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Version = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Use = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Algorithm = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
IsX509Certificate = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
DataProtected = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Keys", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PersistedGrants",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
ConsumedTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PersistedGrants", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PushedAuthorizationRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ReferenceValueHash = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Parameters = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PushedAuthorizationRequests", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerSideSessions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Scheme = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
SubjectId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
DisplayName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Renewed = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Expires = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Data = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerSideSessions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceCodes_DeviceCode",
|
||||
table: "DeviceCodes",
|
||||
column: "DeviceCode",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceCodes_Expiration",
|
||||
table: "DeviceCodes",
|
||||
column: "Expiration");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Keys_Use",
|
||||
table: "Keys",
|
||||
column: "Use");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_ConsumedTime",
|
||||
table: "PersistedGrants",
|
||||
column: "ConsumedTime");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_Expiration",
|
||||
table: "PersistedGrants",
|
||||
column: "Expiration");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_Key",
|
||||
table: "PersistedGrants",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
|
||||
table: "PersistedGrants",
|
||||
columns: new[] { "SubjectId", "ClientId", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
|
||||
table: "PersistedGrants",
|
||||
columns: new[] { "SubjectId", "SessionId", "Type" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PushedAuthorizationRequests_ExpiresAtUtc",
|
||||
table: "PushedAuthorizationRequests",
|
||||
column: "ExpiresAtUtc");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PushedAuthorizationRequests_ReferenceValueHash",
|
||||
table: "PushedAuthorizationRequests",
|
||||
column: "ReferenceValueHash",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_DisplayName",
|
||||
table: "ServerSideSessions",
|
||||
column: "DisplayName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_Expires",
|
||||
table: "ServerSideSessions",
|
||||
column: "Expires");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_Key",
|
||||
table: "ServerSideSessions",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_SessionId",
|
||||
table: "ServerSideSessions",
|
||||
column: "SessionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerSideSessions_SubjectId",
|
||||
table: "ServerSideSessions",
|
||||
column: "SubjectId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DeviceCodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Keys");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PersistedGrants");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PushedAuthorizationRequests");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerSideSessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Microser.IdS.Migrations.PersistedGrantDb
|
||||
{
|
||||
[DbContext(typeof(PersistedGrantDbContext))]
|
||||
partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
|
||||
{
|
||||
b.Property<string>("UserCode")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserCode");
|
||||
|
||||
b.HasIndex("DeviceCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.ToTable("DeviceCodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Algorithm")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DataProtected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsX509Certificate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Use")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Use");
|
||||
|
||||
b.ToTable("Keys", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ConsumedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expiration")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ConsumedTime");
|
||||
|
||||
b.HasIndex("Expiration");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SubjectId", "ClientId", "Type");
|
||||
|
||||
b.HasIndex("SubjectId", "SessionId", "Type");
|
||||
|
||||
b.ToTable("PersistedGrants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("ExpiresAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ReferenceValueHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAtUtc");
|
||||
|
||||
b.HasIndex("ReferenceValueHash")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("PushedAuthorizationRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("Expires")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Renewed")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Scheme")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SubjectId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayName");
|
||||
|
||||
b.HasIndex("Expires");
|
||||
|
||||
b.HasIndex("Key")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.HasIndex("SubjectId");
|
||||
|
||||
b.ToTable("ServerSideSessions", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Microser/Microser.IdS/Pages/Account/AccessDenied.cshtml
Normal file
10
src/Microser/Microser.IdS/Pages/Account/AccessDenied.cshtml
Normal file
@@ -0,0 +1,10 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Account.AccessDeniedModel
|
||||
@{
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>Access Denied</h1>
|
||||
<p>You do not have permission to access that resource.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Account
|
||||
{
|
||||
public class AccessDeniedModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml
Normal file
40
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml
Normal file
@@ -0,0 +1,40 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Create.Index
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Create Account</h1>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-6">
|
||||
<form asp-page="/Account/Create/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Name"></label>
|
||||
<input type="text" class="form-control" placeholder="Name" asp-for="Input.Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Email"></label>
|
||||
<input type="email" class="form-control" placeholder="Email" asp-for="Input.Email" >
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" name="Input.Button" value="create">Create</button>
|
||||
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
122
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml.cs
Normal file
122
src/Microser/Microser.IdS/Pages/Account/Create/Index.cshtml.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Test;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Create
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
TestUserStore? users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
}
|
||||
|
||||
public IActionResult OnGet(string? returnUrl)
|
||||
{
|
||||
Input = new InputModel { ReturnUrl = returnUrl };
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// check if we are in the context of an authorization request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
|
||||
// the user clicked the "cancel" button
|
||||
if (Input.Button != "create")
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
// if the user cancels, send a result back into IdentityServer as if they
|
||||
// denied the consent (even if this client does not require consent).
|
||||
// this will send back an access denied OIDC error response to the client.
|
||||
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// since we don't have a valid context, then we just go back to the home page
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
|
||||
if (_users.FindByUsername(Input.Username) != null)
|
||||
{
|
||||
ModelState.AddModelError("Input.Username", "Invalid username");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = _users.CreateUser(Input.Username, Input.Password, Input.Name, Input.Email);
|
||||
|
||||
// issue authentication cookie with subject ID and username
|
||||
var isuser = new IdentityServerUser(user.SubjectId)
|
||||
{
|
||||
DisplayName = user.Username
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(isuser);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we can trust Input.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microser/Microser.IdS/Pages/Account/Create/InputModel.cs
Normal file
23
src/Microser/Microser.IdS/Pages/Account/Create/InputModel.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Create
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
public string? Button { get; set; }
|
||||
}
|
||||
}
|
||||
89
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml
Normal file
89
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml
Normal file
@@ -0,0 +1,89 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Login.Index
|
||||
|
||||
<div class="login-page">
|
||||
<div class="lead">
|
||||
<h1>Login</h1>
|
||||
<p>Choose how to login</p>
|
||||
</div>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
|
||||
@if (Model.View.EnableLocalLogin)
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Local Account</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form asp-page="/Account/Login/Index">
|
||||
<input type="hidden" asp-for="Input.ReturnUrl" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Username"></label>
|
||||
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Input.Password"></label>
|
||||
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
|
||||
</div>
|
||||
|
||||
@if (Model.View.AllowRememberLogin)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" asp-for="Input.RememberLogin">
|
||||
<label class="form-check-label" asp-for="Input.RememberLogin">
|
||||
Remember My Login
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-primary" name="Input.Button" value="login">Login</button>
|
||||
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="col-sm-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>External Account</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-inline">
|
||||
@foreach (var provider in Model.View.VisibleExternalProviders)
|
||||
{
|
||||
<li class="list-inline-item">
|
||||
<a class="btn btn-secondary"
|
||||
asp-page="/ExternalLogin/Challenge"
|
||||
asp-route-scheme="@provider.AuthenticationScheme"
|
||||
asp-route-returnUrl="@Model.Input.ReturnUrl">
|
||||
@provider.DisplayName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.View.EnableLocalLogin && !Model.View.VisibleExternalProviders.Any())
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Invalid login request</strong>
|
||||
There are no login schemes configured for this request.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
231
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml.cs
Normal file
231
src/Microser/Microser.IdS/Pages/Account/Login/Index.cshtml.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using Duende.IdentityServer.Test;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly TestUserStore _users;
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly IIdentityProviderStore _identityProviderStore;
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public Index(
|
||||
IIdentityServerInteractionService interaction,
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IIdentityProviderStore identityProviderStore,
|
||||
IEventService events,
|
||||
TestUserStore? users = null)
|
||||
{
|
||||
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
|
||||
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
|
||||
|
||||
_interaction = interaction;
|
||||
_schemeProvider = schemeProvider;
|
||||
_identityProviderStore = identityProviderStore;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? returnUrl)
|
||||
{
|
||||
await BuildModelAsync(returnUrl);
|
||||
|
||||
if (View.IsExternalLoginOnly)
|
||||
{
|
||||
// we only have one option for logging in and it's an external provider
|
||||
return RedirectToPage("/ExternalLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// check if we are in the context of an authorization request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
|
||||
|
||||
// the user clicked the "cancel" button
|
||||
if (Input.Button != "login")
|
||||
{
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
// if the user cancels, send a result back into IdentityServer as if they
|
||||
// denied the consent (even if this client does not require consent).
|
||||
// this will send back an access denied OIDC error response to the client.
|
||||
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// since we don't have a valid context, then we just go back to the home page
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// validate username/password against in-memory store
|
||||
if (_users.ValidateCredentials(Input.Username, Input.Password))
|
||||
{
|
||||
var user = _users.FindByUsername(Input.Username);
|
||||
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);
|
||||
|
||||
// only set explicit expiration here if user chooses "remember me".
|
||||
// otherwise we rely upon expiration configured in cookie middleware.
|
||||
var props = new AuthenticationProperties();
|
||||
if (LoginOptions.AllowRememberLogin && Input.RememberLogin)
|
||||
{
|
||||
props.IsPersistent = true;
|
||||
props.ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration);
|
||||
};
|
||||
|
||||
// issue authentication cookie with subject ID and username
|
||||
var isuser = new IdentityServerUser(user.SubjectId)
|
||||
{
|
||||
DisplayName = user.Username
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(isuser, props);
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
// This "can't happen", because if the ReturnUrl was null, then the context would be null
|
||||
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
|
||||
|
||||
if (context.IsNativeClient())
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
return this.LoadingPage(Input.ReturnUrl);
|
||||
}
|
||||
|
||||
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||
return Redirect(Input.ReturnUrl ?? "~/");
|
||||
}
|
||||
|
||||
// request for a local page
|
||||
if (Url.IsLocalUrl(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect(Input.ReturnUrl);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Input.ReturnUrl))
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
else
|
||||
{
|
||||
// user might have clicked on a malicious link - should be logged
|
||||
throw new ArgumentException("invalid return URL");
|
||||
}
|
||||
}
|
||||
|
||||
const string error = "invalid credentials";
|
||||
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId: context?.Client.ClientId));
|
||||
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
|
||||
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
|
||||
}
|
||||
|
||||
// something went wrong, show form with error
|
||||
await BuildModelAsync(Input.ReturnUrl);
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task BuildModelAsync(string? returnUrl)
|
||||
{
|
||||
Input = new InputModel
|
||||
{
|
||||
ReturnUrl = returnUrl
|
||||
};
|
||||
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
{
|
||||
var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider;
|
||||
|
||||
// this is meant to short circuit the UI and only trigger the one external IdP
|
||||
View = new ViewModel
|
||||
{
|
||||
EnableLocalLogin = local,
|
||||
};
|
||||
|
||||
Input.Username = context.LoginHint;
|
||||
|
||||
if (!local)
|
||||
{
|
||||
View.ExternalProviders = new[] { new ViewModel.ExternalProvider(authenticationScheme: context.IdP) };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var schemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
|
||||
var providers = schemes
|
||||
.Where(x => x.DisplayName != null)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Name,
|
||||
displayName: x.DisplayName ?? x.Name
|
||||
)).ToList();
|
||||
|
||||
var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())
|
||||
.Where(x => x.Enabled)
|
||||
.Select(x => new ViewModel.ExternalProvider
|
||||
(
|
||||
authenticationScheme: x.Scheme,
|
||||
displayName: x.DisplayName ?? x.Scheme
|
||||
));
|
||||
providers.AddRange(dynamicSchemes);
|
||||
|
||||
var allowLocal = true;
|
||||
var client = context?.Client;
|
||||
if (client != null)
|
||||
{
|
||||
allowLocal = client.EnableLocalLogin;
|
||||
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Count != 0)
|
||||
{
|
||||
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
View = new ViewModel
|
||||
{
|
||||
AllowRememberLogin = LoginOptions.AllowRememberLogin,
|
||||
EnableLocalLogin = allowLocal && LoginOptions.AllowLocalLogin,
|
||||
ExternalProviders = providers.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Microser/Microser.IdS/Pages/Account/Login/InputModel.cs
Normal file
20
src/Microser/Microser.IdS/Pages/Account/Login/InputModel.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public class InputModel
|
||||
{
|
||||
[Required]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string? Password { get; set; }
|
||||
|
||||
public bool RememberLogin { get; set; }
|
||||
public string? ReturnUrl { get; set; }
|
||||
public string? Button { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public static class LoginOptions
|
||||
{
|
||||
public static readonly bool AllowLocalLogin = true;
|
||||
public static readonly bool AllowRememberLogin = true;
|
||||
public static readonly TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
|
||||
public static readonly string InvalidCredentialsErrorMessage = "Invalid username or password";
|
||||
}
|
||||
}
|
||||
29
src/Microser/Microser.IdS/Pages/Account/Login/ViewModel.cs
Normal file
29
src/Microser/Microser.IdS/Pages/Account/Login/ViewModel.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Login
|
||||
{
|
||||
public class ViewModel
|
||||
{
|
||||
public bool AllowRememberLogin { get; set; } = true;
|
||||
public bool EnableLocalLogin { get; set; } = true;
|
||||
|
||||
public IEnumerable<ViewModel.ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();
|
||||
public IEnumerable<ViewModel.ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
|
||||
|
||||
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
|
||||
public string? ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
|
||||
|
||||
public class ExternalProvider
|
||||
{
|
||||
public ExternalProvider(string authenticationScheme, string? displayName = null)
|
||||
{
|
||||
AuthenticationScheme = authenticationScheme;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
public string AuthenticationScheme { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml
Normal file
17
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml
Normal file
@@ -0,0 +1,17 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Logout.Index
|
||||
|
||||
<div class="logout-page">
|
||||
<div class="lead">
|
||||
<h1>Logout</h1>
|
||||
<p>Would you like to logout of IdentityServer?</p>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Account/Logout/Index">
|
||||
<input type="hidden" name="logoutId" value="@Model.LogoutId" />
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
101
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml.cs
Normal file
101
src/Microser/Microser.IdS/Pages/Account/Logout/Index.cshtml.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class Index : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
|
||||
[BindProperty]
|
||||
public string? LogoutId { get; set; }
|
||||
|
||||
public Index(IIdentityServerInteractionService interaction, IEventService events)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGet(string? logoutId)
|
||||
{
|
||||
LogoutId = logoutId;
|
||||
|
||||
var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;
|
||||
|
||||
if (User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
// if the user is not authenticated, then just show logged out page
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var context = await _interaction.GetLogoutContextAsync(LogoutId);
|
||||
if (context?.ShowSignoutPrompt == false)
|
||||
{
|
||||
// it's safe to automatically sign-out
|
||||
showLogoutPrompt = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (showLogoutPrompt == false)
|
||||
{
|
||||
// if the request for logout was properly authenticated from IdentityServer, then
|
||||
// we don't need to show the prompt and can just log the user out directly.
|
||||
return await OnPost();
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
// if there's no current logout context, we need to create one
|
||||
// this captures necessary info from the current logged in user
|
||||
// this can still return null if there is no context needed
|
||||
LogoutId ??= await _interaction.CreateLogoutContextAsync();
|
||||
|
||||
// delete local authentication cookie
|
||||
await HttpContext.SignOutAsync();
|
||||
|
||||
// see if we need to trigger federated logout
|
||||
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||
|
||||
// raise the logout event
|
||||
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
|
||||
Telemetry.Metrics.UserLogout(idp);
|
||||
|
||||
// if it's a local login we can ignore this workflow
|
||||
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
|
||||
{
|
||||
// we need to see if the provider supports external logout
|
||||
if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
|
||||
{
|
||||
// build a return URL so the upstream provider will redirect back
|
||||
// to us after the user has logged out. this allows us to then
|
||||
// complete our single sign-out processing.
|
||||
var url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
|
||||
|
||||
// this triggers a redirect to the external provider for sign-out
|
||||
return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Logout.LoggedOut
|
||||
|
||||
<div class="logged-out-page">
|
||||
<h1>
|
||||
Logout
|
||||
<small>You are now logged out</small>
|
||||
</h1>
|
||||
|
||||
@if (Model.View.PostLogoutRedirectUri != null)
|
||||
{
|
||||
<div>
|
||||
Click <a class="PostLogoutRedirectUri" href="@Model.View.PostLogoutRedirectUri">here</a> to return to the
|
||||
<span>@Model.View.ClientName</span> application.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.SignOutIframeUrl != null)
|
||||
{
|
||||
<iframe width="0" height="0" class="signout" src="@Model.View.SignOutIframeUrl"></iframe>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section scripts
|
||||
{
|
||||
@if (Model.View.AutomaticRedirectAfterSignOut)
|
||||
{
|
||||
<script src="~/js/signout-redirect.js"></script>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[AllowAnonymous]
|
||||
public class LoggedOut : PageModel
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interactionService;
|
||||
|
||||
public LoggedOutViewModel View { get; set; } = default!;
|
||||
|
||||
public LoggedOut(IIdentityServerInteractionService interactionService)
|
||||
{
|
||||
_interactionService = interactionService;
|
||||
}
|
||||
|
||||
public async Task OnGet(string? logoutId)
|
||||
{
|
||||
// get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
|
||||
|
||||
View = new LoggedOutViewModel
|
||||
{
|
||||
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
|
||||
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
|
||||
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
|
||||
SignOutIframeUrl = logout?.SignOutIFrameUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
public class LoggedOutViewModel
|
||||
{
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string? ClientName { get; set; }
|
||||
public string? SignOutIframeUrl { get; set; }
|
||||
public bool AutomaticRedirectAfterSignOut { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Microser.IdS.Pages.Logout
|
||||
{
|
||||
public static class LogoutOptions
|
||||
{
|
||||
public static readonly bool ShowLogoutPrompt = true;
|
||||
public static readonly bool AutomaticRedirectAfterSignOut = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
public class ApiScopeSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class ApiScopeModel : ApiScopeSummaryModel
|
||||
{
|
||||
public string? UserClaims { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class ApiScopeRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public ApiScopeRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApiScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var query = _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.AsQueryable();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new ApiScopeSummaryModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<ApiScopeModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var scope = await _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) return null;
|
||||
|
||||
return new ApiScopeModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(ApiScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = new Duende.IdentityServer.Models.ApiScope()
|
||||
{
|
||||
Name = model.Name,
|
||||
DisplayName = model.DisplayName?.Trim()
|
||||
};
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
if (claims.Any())
|
||||
{
|
||||
scope.UserClaims = claims.ToList();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.ApiScopes.Add(scope.ToEntity());
|
||||
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ApiScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = await _context.ApiScopes
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||
|
||||
if (scope.DisplayName != model.DisplayName)
|
||||
{
|
||||
scope.DisplayName = model.DisplayName?.Trim();
|
||||
}
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||
|
||||
if (claimsToRemove.Length != 0)
|
||||
{
|
||||
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||
}
|
||||
if (claimsToAdd.Length != 0)
|
||||
{
|
||||
scope.UserClaims.AddRange(claimsToAdd.Select(x => new ApiScopeClaim
|
||||
{
|
||||
Type = x,
|
||||
}));
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string id)
|
||||
{
|
||||
var scope = await _context.ApiScopes.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Api Scope");
|
||||
|
||||
_context.ApiScopes.Remove(scope);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Edit.cshtml
Normal file
39
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Edit.cshtml
Normal file
@@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="identityscopes-page">
|
||||
|
||||
<h2>
|
||||
Edit Identity Scope: @Model.InputModel.Name
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public EditModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ApiScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string Button { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/ApiScopes/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/ApiScopes/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Index.cshtml
Normal file
54
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/Index.cshtml
Normal file
@@ -0,0 +1,54 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
API Scopes
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/ApiScopes/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Display Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var scope in Model.Scopes)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/ApiScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||
<td>@scope.DisplayName</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public IndexModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<ApiScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Scopes = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/New.cshtml
Normal file
35
src/Microser/Microser.IdS/Pages/Admin/ApiScopes/New.cshtml
Normal file
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.ApiScopes.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<h2>
|
||||
New API Scope
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.ApiScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly ApiScopeRepository _repository;
|
||||
|
||||
public NewModel(ApiScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ApiScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/ApiScopes/Edit", new { id = InputModel.Name });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
public class ClientSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string ClientId { get; set; } = default!;
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Required]
|
||||
public Flow Flow { get; set; }
|
||||
}
|
||||
|
||||
public class CreateClientModel : ClientSummaryModel
|
||||
{
|
||||
public string Secret { get; set; } = default!;
|
||||
}
|
||||
|
||||
public class ClientModel : CreateClientModel, IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public string AllowedScopes { get; set; } = default!;
|
||||
|
||||
public string? RedirectUri { get; set; }
|
||||
public string? InitiateLoginUri { get; set; }
|
||||
public string? PostLogoutRedirectUri { get; set; }
|
||||
public string? FrontChannelLogoutUri { get; set; }
|
||||
public string? BackChannelLogoutUri { get; set; }
|
||||
|
||||
private static readonly string[] memberNames = new[] { "RedirectUri" };
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
var errors = new List<ValidationResult>();
|
||||
|
||||
if (Flow == Flow.CodeFlowWithPkce)
|
||||
{
|
||||
if (RedirectUri == null)
|
||||
{
|
||||
errors.Add(new ValidationResult("Redirect URI is required.", memberNames));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Flow
|
||||
{
|
||||
ClientCredentials,
|
||||
CodeFlowWithPkce
|
||||
}
|
||||
|
||||
public class ClientRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public ClientRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ClientSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var grants = new[] { GrantType.AuthorizationCode, GrantType.ClientCredentials };
|
||||
|
||||
var query = _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Where(x => x.AllowedGrantTypes.Count == 1 && x.AllowedGrantTypes.Any(grant => grants.Contains(grant.GrantType)));
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.ClientId.Contains(filter) || x.ClientName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new ClientSummaryModel
|
||||
{
|
||||
ClientId = x.ClientId,
|
||||
Name = x.ClientName,
|
||||
Flow = x.AllowedGrantTypes.Select(x => x.GrantType).Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<ClientModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var client = await _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Include(x => x.AllowedScopes)
|
||||
.Include(x => x.RedirectUris)
|
||||
.Include(x => x.PostLogoutRedirectUris)
|
||||
.Where(x => x.ClientId == id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (client == null) return null;
|
||||
|
||||
return new ClientModel
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Name = client.ClientName,
|
||||
Flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce,
|
||||
AllowedScopes = client.AllowedScopes.Count != 0 ? client.AllowedScopes.Select(x => x.Scope).Aggregate((a, b) => $"{a} {b}") : string.Empty,
|
||||
RedirectUri = client.RedirectUris.Select(x => x.RedirectUri).SingleOrDefault(),
|
||||
InitiateLoginUri = client.InitiateLoginUri,
|
||||
PostLogoutRedirectUri = client.PostLogoutRedirectUris.Select(x => x.PostLogoutRedirectUri).SingleOrDefault(),
|
||||
FrontChannelLogoutUri = client.FrontChannelLogoutUri,
|
||||
BackChannelLogoutUri = client.BackChannelLogoutUri,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(CreateClientModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var client = new Duende.IdentityServer.Models.Client();
|
||||
client.ClientId = model.ClientId.Trim();
|
||||
client.ClientName = model.Name?.Trim();
|
||||
|
||||
client.ClientSecrets.Add(new Duende.IdentityServer.Models.Secret(model.Secret.Sha256()));
|
||||
|
||||
if (model.Flow == Flow.ClientCredentials)
|
||||
{
|
||||
client.AllowedGrantTypes = GrantTypes.ClientCredentials;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.AllowedGrantTypes = GrantTypes.Code;
|
||||
client.AllowOfflineAccess = true;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.Clients.Add(client.ToEntity());
|
||||
#pragma warning restore CA1849 // Call async methods when in an async method
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ClientModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var client = await _context.Clients
|
||||
.Include(x => x.AllowedGrantTypes)
|
||||
.Include(x => x.AllowedScopes)
|
||||
.Include(x => x.RedirectUris)
|
||||
.Include(x => x.PostLogoutRedirectUris)
|
||||
.SingleOrDefaultAsync(x => x.ClientId == model.ClientId);
|
||||
|
||||
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||
|
||||
if (client.ClientName != model.Name)
|
||||
{
|
||||
client.ClientName = model.Name?.Trim();
|
||||
}
|
||||
|
||||
var scopes = model.AllowedScopes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||
var currentScopes = (client.AllowedScopes.Select(x => x.Scope) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var scopesToAdd = scopes.Except(currentScopes).ToArray();
|
||||
var scopesToRemove = currentScopes.Except(scopes).ToArray();
|
||||
|
||||
if (scopesToRemove.Length != 0)
|
||||
{
|
||||
client.AllowedScopes.RemoveAll(x => scopesToRemove.Contains(x.Scope));
|
||||
}
|
||||
if (scopesToAdd.Length != 0)
|
||||
{
|
||||
client.AllowedScopes.AddRange(scopesToAdd.Select(x => new ClientScope
|
||||
{
|
||||
Scope = x,
|
||||
}));
|
||||
}
|
||||
|
||||
var flow = client.AllowedGrantTypes.Select(x => x.GrantType)
|
||||
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce;
|
||||
|
||||
if (flow == Flow.CodeFlowWithPkce)
|
||||
{
|
||||
if (client.RedirectUris.SingleOrDefault()?.RedirectUri != model.RedirectUri)
|
||||
{
|
||||
client.RedirectUris.Clear();
|
||||
if (model.RedirectUri != null)
|
||||
{
|
||||
client.RedirectUris.Add(new ClientRedirectUri { RedirectUri = model.RedirectUri.Trim() });
|
||||
}
|
||||
}
|
||||
if (client.InitiateLoginUri != model.InitiateLoginUri)
|
||||
{
|
||||
client.InitiateLoginUri = model.InitiateLoginUri;
|
||||
}
|
||||
if (client.PostLogoutRedirectUris.SingleOrDefault()?.PostLogoutRedirectUri != model.PostLogoutRedirectUri)
|
||||
{
|
||||
client.PostLogoutRedirectUris.Clear();
|
||||
if (model.PostLogoutRedirectUri != null)
|
||||
{
|
||||
client.PostLogoutRedirectUris.Add(new ClientPostLogoutRedirectUri { PostLogoutRedirectUri = model.PostLogoutRedirectUri.Trim() });
|
||||
}
|
||||
}
|
||||
if (client.FrontChannelLogoutUri != model.FrontChannelLogoutUri)
|
||||
{
|
||||
client.FrontChannelLogoutUri = model.FrontChannelLogoutUri?.Trim();
|
||||
}
|
||||
if (client.BackChannelLogoutUri != model.BackChannelLogoutUri)
|
||||
{
|
||||
client.BackChannelLogoutUri = model.BackChannelLogoutUri?.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string clientId)
|
||||
{
|
||||
var client = await _context.Clients.SingleOrDefaultAsync(x => x.ClientId == clientId);
|
||||
|
||||
if (client == null) throw new ArgumentException("Invalid Client Id");
|
||||
|
||||
_context.Clients.Remove(client);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml
Normal file
65
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml
Normal file
@@ -0,0 +1,65 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
|
||||
<h2>
|
||||
Edit Client Id: @Model.InputModel.ClientId
|
||||
(<small>@Model.InputModel.Flow</small>)
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.ClientId" />
|
||||
<input type="hidden" asp-for="@Model.InputModel.Flow" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.AllowedScopes">Allowed Scopes (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.AllowedScopes" />
|
||||
</div>
|
||||
|
||||
@if (Model.InputModel.Flow == Microser.IdS.Pages.Admin.Clients.Flow.CodeFlowWithPkce)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.RedirectUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.RedirectUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.InitiateLoginUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.InitiateLoginUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.PostLogoutRedirectUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.PostLogoutRedirectUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.FrontChannelLogoutUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.FrontChannelLogoutUri" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.BackChannelLogoutUri"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.BackChannelLogoutUri" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
58
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml.cs
Normal file
58
src/Microser/Microser.IdS/Pages/Admin/Clients/Edit.cshtml.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public EditModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public ClientModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string? Button { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/Clients/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/Clients/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/Clients/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Microser/Microser.IdS/Pages/Admin/Clients/Index.cshtml
Normal file
56
src/Microser/Microser.IdS/Pages/Admin/Clients/Index.cshtml
Normal file
@@ -0,0 +1,56 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
Clients
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/Clients/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Name</th>
|
||||
<th>Flow</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var client in Model.Clients)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/Clients/Edit" asp-route-id="@client.ClientId">@client.ClientId</a></td>
|
||||
<td>@client.Name</td>
|
||||
<td>@client.Flow</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public IndexModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<ClientSummaryModel> Clients { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Clients = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml
Normal file
56
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml
Normal file
@@ -0,0 +1,56 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.Clients.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
@if (Model.Created)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>Client Id <em>@Model.InputModel.ClientId</em> created</h2>
|
||||
<p>The client secret is displayed below. Copy it now, as it won't be shown again.</p>
|
||||
<p class="alert alert-danger">@Model.InputModel.Secret</p>
|
||||
<p>Click here to <a asp-page="/Admin/Clients/Edit" asp-route-id="@Model.InputModel.ClientId">continue</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h2>
|
||||
New Client
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.ClientId"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.ClientId" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Secret"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Secret" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Flow"></label>
|
||||
<select class="form-control" asp-for="@Model.InputModel.Flow">
|
||||
<option value="0">Client Credentials</option>
|
||||
<option value="1">Code Flow (with PKCE)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
46
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml.cs
Normal file
46
src/Microser/Microser.IdS/Pages/Admin/Clients/New.cshtml.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.Clients
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly ClientRepository _repository;
|
||||
|
||||
public NewModel(ClientRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public CreateClientModel InputModel { get; set; } = default!;
|
||||
|
||||
public bool Created { get; set; }
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
InputModel = new CreateClientModel
|
||||
{
|
||||
Secret = Convert.ToBase64String(CryptoRandom.CreateRandomKey(16))
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
Created = true;
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.EditModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="identityscopes-page">
|
||||
|
||||
<h2>
|
||||
Edit Identity Scope: @Model.InputModel.Name
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="@Model.InputModel.Name" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class EditModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public EditModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public string? Button { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(string id)
|
||||
{
|
||||
var model = await _repository.GetByIdAsync(id);
|
||||
if (model == null)
|
||||
{
|
||||
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||
}
|
||||
else
|
||||
{
|
||||
InputModel = model;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync(string id)
|
||||
{
|
||||
if (Button == "delete")
|
||||
{
|
||||
await _repository.DeleteAsync(id);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Index");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.UpdateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.EntityFramework.DbContexts;
|
||||
using Duende.IdentityServer.EntityFramework.Entities;
|
||||
using Duende.IdentityServer.EntityFramework.Mappers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
public class IdentityScopeSummaryModel
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
|
||||
public class IdentityScopeModel : IdentityScopeSummaryModel
|
||||
{
|
||||
public string? UserClaims { get; set; }
|
||||
}
|
||||
|
||||
public class IdentityScopeRepository
|
||||
{
|
||||
private readonly ConfigurationDbContext _context;
|
||||
|
||||
public IdentityScopeRepository(ConfigurationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<IdentityScopeSummaryModel>> GetAllAsync(string? filter = null)
|
||||
{
|
||||
var query = _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.AsQueryable();
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
|
||||
}
|
||||
|
||||
var result = query.Select(x => new IdentityScopeSummaryModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName
|
||||
});
|
||||
|
||||
return await result.ToArrayAsync();
|
||||
}
|
||||
|
||||
public async Task<IdentityScopeModel?> GetByIdAsync(string id)
|
||||
{
|
||||
var scope = await _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) return null;
|
||||
|
||||
return new IdentityScopeModel
|
||||
{
|
||||
Name = scope.Name,
|
||||
DisplayName = scope.DisplayName,
|
||||
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task CreateAsync(IdentityScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = new Duende.IdentityServer.Models.IdentityResource()
|
||||
{
|
||||
Name = model.Name,
|
||||
DisplayName = model.DisplayName?.Trim()
|
||||
};
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
if (claims.Any())
|
||||
{
|
||||
scope.UserClaims = claims.ToList();
|
||||
}
|
||||
#pragma warning disable CA1849 // Call async methods when in an async method
|
||||
// CA1849 Suppressed because AddAsync is only needed for value generators that
|
||||
// need async database access (e.g., HiLoValueGenerator), and we don't use those
|
||||
// generators
|
||||
_context.IdentityResources.Add(scope.ToEntity());
|
||||
#pragma warning restore CA1849
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(IdentityScopeModel model)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
var scope = await _context.IdentityResources
|
||||
.Include(x => x.UserClaims)
|
||||
.SingleOrDefaultAsync(x => x.Name == model.Name);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||
|
||||
if (scope.DisplayName != model.DisplayName)
|
||||
{
|
||||
scope.DisplayName = model.DisplayName?.Trim();
|
||||
}
|
||||
|
||||
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
|
||||
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
|
||||
|
||||
var claimsToAdd = claims.Except(currentClaims).ToArray();
|
||||
var claimsToRemove = currentClaims.Except(claims).ToArray();
|
||||
|
||||
if (claimsToRemove.Length != 0)
|
||||
{
|
||||
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
|
||||
}
|
||||
if (claimsToAdd.Length != 0)
|
||||
{
|
||||
scope.UserClaims.AddRange(claimsToAdd.Select(x => new IdentityResourceClaim
|
||||
{
|
||||
Type = x,
|
||||
}));
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string id)
|
||||
{
|
||||
var scope = await _context.IdentityResources.SingleOrDefaultAsync(x => x.Name == id);
|
||||
|
||||
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
|
||||
|
||||
_context.IdentityResources.Remove(scope);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<div class="row">
|
||||
<h2>
|
||||
Identity Scopes
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="row">
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
|
||||
<a asp-page="/Admin/IdentityScopes/New" class="btn btn-success">New</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Display Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach(var scope in Model.Scopes)
|
||||
{
|
||||
<tr>
|
||||
<td><a asp-page="/Admin/IdentityScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
|
||||
<td>@scope.DisplayName</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public IndexModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public IEnumerable<IdentityScopeSummaryModel> Scopes { get; private set; } = default!;
|
||||
public string? Filter { get; set; }
|
||||
|
||||
public async Task OnGetAsync(string? filter)
|
||||
{
|
||||
Filter = filter;
|
||||
Scopes = await _repository.GetAllAsync(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IdentityScopes.NewModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="clients-page">
|
||||
<h2>
|
||||
New Identity Scope
|
||||
</h2>
|
||||
|
||||
<partial name="_ValidationSummary" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.Name"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.DisplayName"></label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
|
||||
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary" type="submit">Save Changes</button>
|
||||
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin.IdentityScopes
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class NewModel : PageModel
|
||||
{
|
||||
private readonly IdentityScopeRepository _repository;
|
||||
|
||||
public NewModel(IdentityScopeRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[BindProperty]
|
||||
public IdentityScopeModel InputModel { get; set; } = default!;
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _repository.CreateAsync(InputModel);
|
||||
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id = InputModel.Name });
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml
Normal file
23
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml
Normal file
@@ -0,0 +1,23 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Admin.IndexModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="admin-page">
|
||||
<h2>
|
||||
Duende IdentityServer Admin
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<p>There are simple administrative pages.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="list-group">
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/Clients/Index">Clients</a>
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/IdentityScopes/Index">Identity Scopes</a>
|
||||
<a class="list-group-item list-group-item-action" asp-page="/Admin/ApiScopes/Index">API Scopes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
17
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml.cs
Normal file
17
src/Microser/Microser.IdS/Pages/Admin/Index.cshtml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Admin
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml
Normal file
48
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml
Normal file
@@ -0,0 +1,48 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Ciba.AllModel
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-page">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Pending Backchannel Login Requests</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (Model.Logins.Any())
|
||||
{
|
||||
<table class="table table-bordered table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Client Id</th>
|
||||
<th>Binding Message</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var login in Model.Logins)
|
||||
{
|
||||
<tr>
|
||||
<td>@login.InternalId</td>
|
||||
<td>@login.Client.ClientId</td>
|
||||
<td>@login.BindingMessage</td>
|
||||
<td>
|
||||
<a asp-page="Consent" asp-route-id="@login.InternalId" class="btn btn-primary">Process</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>No Pending Login Requests</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
29
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml.cs
Normal file
29
src/Microser/Microser.IdS/Pages/Ciba/All.cshtml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class AllModel : PageModel
|
||||
{
|
||||
public IEnumerable<BackchannelUserLoginRequest> Logins { get; set; } = default!;
|
||||
|
||||
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
|
||||
|
||||
public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService)
|
||||
{
|
||||
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
|
||||
}
|
||||
|
||||
public async Task OnGet()
|
||||
{
|
||||
Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml
Normal file
98
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml
Normal file
@@ -0,0 +1,98 @@
|
||||
@page
|
||||
@model Microser.IdS.Pages.Ciba.Consent
|
||||
@{
|
||||
}
|
||||
|
||||
<div class="ciba-consent">
|
||||
<div class="lead">
|
||||
@if (Model.View.ClientLogoUrl != null)
|
||||
{
|
||||
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
|
||||
}
|
||||
<h1>
|
||||
@Model.View.ClientName
|
||||
<small class="text-muted">is requesting your permission</small>
|
||||
</h1>
|
||||
|
||||
<h3>Verify that this identifier matches what the client is displaying: <em class="text-primary">@Model.View.BindingMessage</em></h3>
|
||||
|
||||
<p>Uncheck the permissions you do not wish to grant.</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<partial name="_ValidationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form asp-page="/Ciba/Consent">
|
||||
<input type="hidden" asp-for="Input.Id" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
@if (Model.View.IdentityScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
Personal Information
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.IdentityScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="@scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.View.ApiScopes.Any())
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-tasks"></span>
|
||||
Application Access
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var scope in Model.View.ApiScopes)
|
||||
{
|
||||
<partial name="_ScopeListItem" model="scope" />
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="glyphicon glyphicon-pencil"></span>
|
||||
Description
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
|
||||
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
|
||||
</div>
|
||||
<div class="col-sm-4 col-lg-auto">
|
||||
@if (Model.View.ClientUrl != null)
|
||||
{
|
||||
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
<strong>@Model.View.ClientName</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
229
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml.cs
Normal file
229
src/Microser/Microser.IdS/Pages/Ciba/Consent.cshtml.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.IdentityServer.Events;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Ciba
|
||||
{
|
||||
[Authorize]
|
||||
[SecurityHeaders]
|
||||
public class Consent : PageModel
|
||||
{
|
||||
private readonly IBackchannelAuthenticationInteractionService _interaction;
|
||||
private readonly IEventService _events;
|
||||
private readonly ILogger<Consent> _logger;
|
||||
|
||||
public Consent(
|
||||
IBackchannelAuthenticationInteractionService interaction,
|
||||
IEventService events,
|
||||
ILogger<Consent> logger)
|
||||
{
|
||||
_interaction = interaction;
|
||||
_events = events;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ViewModel View { get; set; } = default!;
|
||||
|
||||
[BindProperty]
|
||||
public InputModel Input { get; set; } = default!;
|
||||
|
||||
public async Task<IActionResult> OnGet(string? id)
|
||||
{
|
||||
if (!await SetViewModelAsync(id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
Input = new InputModel
|
||||
{
|
||||
Id = id
|
||||
};
|
||||
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPost()
|
||||
{
|
||||
// validate return url is still valid
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(Input.Id ?? throw new ArgumentNullException(nameof(Input.Id)));
|
||||
if (request == null || request.Subject.GetSubjectId() != User.GetSubjectId())
|
||||
{
|
||||
_logger.InvalidId(Input.Id);
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
|
||||
CompleteBackchannelLoginRequest? result = null;
|
||||
|
||||
// user clicked 'no' - send back the standard 'access_denied' response
|
||||
if (Input.Button == "no")
|
||||
{
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id);
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
|
||||
}
|
||||
// user clicked 'yes' - validate the data
|
||||
else if (Input.Button == "yes")
|
||||
{
|
||||
// if the user consented to some scope, build the response model
|
||||
if (Input.ScopesConsented.Any())
|
||||
{
|
||||
var scopes = Input.ScopesConsented;
|
||||
if (ConsentOptions.EnableOfflineAccess == false)
|
||||
{
|
||||
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
|
||||
}
|
||||
|
||||
result = new CompleteBackchannelLoginRequest(Input.Id)
|
||||
{
|
||||
ScopesValuesConsented = scopes.ToArray(),
|
||||
Description = Input.Description
|
||||
};
|
||||
|
||||
// emit event
|
||||
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false));
|
||||
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, result.ScopesValuesConsented, false);
|
||||
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
|
||||
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
// communicate outcome of consent back to identityserver
|
||||
await _interaction.CompleteLoginRequestAsync(result);
|
||||
|
||||
return RedirectToPage("/Ciba/All");
|
||||
}
|
||||
|
||||
// we need to redisplay the consent UI
|
||||
if (!await SetViewModelAsync(Input.Id))
|
||||
{
|
||||
return RedirectToPage("/Home/Error/Index");
|
||||
}
|
||||
return Page();
|
||||
}
|
||||
|
||||
private async Task<bool> SetViewModelAsync(string? id)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(id);
|
||||
|
||||
var request = await _interaction.GetLoginRequestByInternalIdAsync(id);
|
||||
if (request != null && request.Subject.GetSubjectId() == User.GetSubjectId())
|
||||
{
|
||||
View = CreateConsentViewModel(request);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoMatchingBackchannelLoginRequest(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewModel CreateConsentViewModel(BackchannelUserLoginRequest request)
|
||||
{
|
||||
var vm = new ViewModel
|
||||
{
|
||||
ClientName = request.Client.ClientName ?? request.Client.ClientId,
|
||||
ClientUrl = request.Client.ClientUri,
|
||||
ClientLogoUrl = request.Client.LogoUri,
|
||||
BindingMessage = request.BindingMessage
|
||||
};
|
||||
|
||||
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources
|
||||
.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name)))
|
||||
.ToArray();
|
||||
|
||||
var resourceIndicators = request.RequestedResourceIndicators ?? Enumerable.Empty<string>();
|
||||
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
|
||||
|
||||
var apiScopes = new List<ScopeViewModel>();
|
||||
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
|
||||
{
|
||||
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
|
||||
if (apiScope != null)
|
||||
{
|
||||
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
|
||||
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
|
||||
.Select(x => new ResourceViewModel
|
||||
{
|
||||
Name = x.Name,
|
||||
DisplayName = x.DisplayName ?? x.Name,
|
||||
}).ToArray();
|
||||
apiScopes.Add(scopeVm);
|
||||
}
|
||||
}
|
||||
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
|
||||
{
|
||||
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
|
||||
}
|
||||
vm.ApiScopes = apiScopes;
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = identity.Name,
|
||||
Value = identity.Name,
|
||||
DisplayName = identity.DisplayName ?? identity.Name,
|
||||
Description = identity.Description,
|
||||
Emphasize = identity.Emphasize,
|
||||
Required = identity.Required,
|
||||
Checked = check || identity.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
|
||||
{
|
||||
var displayName = apiScope.DisplayName ?? apiScope.Name;
|
||||
if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
|
||||
{
|
||||
displayName += ":" + parsedScopeValue.ParsedParameter;
|
||||
}
|
||||
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Name = parsedScopeValue.ParsedName,
|
||||
Value = parsedScopeValue.RawValue,
|
||||
DisplayName = displayName,
|
||||
Description = apiScope.Description,
|
||||
Emphasize = apiScope.Emphasize,
|
||||
Required = apiScope.Required,
|
||||
Checked = check || apiScope.Required
|
||||
};
|
||||
}
|
||||
|
||||
private static ScopeViewModel GetOfflineAccessScope(bool check)
|
||||
{
|
||||
return new ScopeViewModel
|
||||
{
|
||||
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
|
||||
DisplayName = ConsentOptions.OfflineAccessDisplayName,
|
||||
Description = ConsentOptions.OfflineAccessDescription,
|
||||
Emphasize = true,
|
||||
Checked = check
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user