added Microser

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

View File

@@ -0,0 +1,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();
}
}

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
@using Microser.BlazorAppClient.Services
@using Microser.Core.Models
@using Microsoft.AspNetCore.Authorization
@inject NavigationManager NavigationManager
@inject IWeatherForecastApiService WeatherForecastApiService
@attribute [Authorize]

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

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

View File

@@ -0,0 +1,88 @@
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using IdentityModel;
namespace Microser.IdS
{
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Address(),
new IdentityResources.Email(),
new IdentityResource(
"roles",
"Your role(s)",
new List<string>(){ JwtClaimTypes.Role })
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("scope1"),
new ApiScope("scope2"),
new ApiScope("microser_api_weather"),
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "dotnet_blazor_serverapp",
ClientName = "Blazor Server App",
ClientSecrets = {
new Secret("E8C65E41BB0E4E519D409023CF5112F4".Sha256())
},
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = true,
AllowedCorsOrigins = { "https://localhost:7001" },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Email,
"roles",
"scope1",
"microser_api_weather"
},
RedirectUris = { "https://localhost:7001/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:7001/signout-callback-oidc" },
Enabled = true
},
// m2m client credentials flow client
new Client
{
ClientId = "m2m.client",
ClientName = "Client Credentials Client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) },
AllowedScopes = { "scope1" }
},
// interactive client using code flow + pkce
new Client
{
ClientId = "interactive",
ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:44300/signin-oidc" },
FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "scope2" }
},
};
}
}

View File

@@ -0,0 +1,106 @@
using Duende.IdentityServer;
using Microser.IdS.Pages.Admin.ApiScopes;
using Microser.IdS.Pages.Admin.Clients;
using Microser.IdS.Pages.Admin.IdentityScopes;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace Microser.IdS
{
internal static class HostingExtensions
{
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
{
builder.Services.AddRazorPages();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var isBuilder = builder.Services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://docs.duendesoftware.com/identityserver/v5/fundamentals/resources/
options.EmitStaticAudienceClaim = true;
})
.AddTestUsers(TestUsers.Users)
// this adds the config data from DB (clients, resources, CORS)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
})
// this is something you will want in production to reduce load on and requests to the DB
//.AddConfigurationStoreCache()
//
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
});
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to https://localhost:5001/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
});
// this adds the necessary config for the simple admin/config pages
{
builder.Services.AddAuthorization(options =>
options.AddPolicy("admin",
policy => policy.RequireClaim("sub", "1"))
);
builder.Services.Configure<RazorPagesOptions>(options =>
options.Conventions.AuthorizeFolder("/Admin", "admin"));
builder.Services.AddTransient<Microser.IdS.Pages.Portal.ClientRepository>();
builder.Services.AddTransient<ClientRepository>();
builder.Services.AddTransient<IdentityScopeRepository>();
builder.Services.AddTransient<ApiScopeRepository>();
}
// if you want to use server-side sessions: https://blog.duendesoftware.com/posts/20220406_session_management/
// then enable it
//isBuilder.AddServerSideSessions();
//
// and put some authorization on the admin/management pages using the same policy created above
//builder.Services.Configure<RazorPagesOptions>(options =>
// options.Conventions.AuthorizeFolder("/ServerSideSessions", "admin"));
return builder.Build();
}
public static WebApplication ConfigurePipeline(this WebApplication app)
{
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapRazorPages()
.RequireAuthorization();
return app;
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,301 @@
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
BEGIN TRANSACTION;
CREATE TABLE "ApiResources" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResources" PRIMARY KEY AUTOINCREMENT,
"Enabled" INTEGER NOT NULL,
"Name" TEXT NOT NULL,
"DisplayName" TEXT NULL,
"Description" TEXT NULL,
"AllowedAccessTokenSigningAlgorithms" TEXT NULL,
"ShowInDiscoveryDocument" INTEGER NOT NULL,
"RequireResourceIndicator" INTEGER NOT NULL,
"Created" TEXT NOT NULL,
"Updated" TEXT NULL,
"LastAccessed" TEXT NULL,
"NonEditable" INTEGER NOT NULL
);
CREATE TABLE "ApiScopes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopes" PRIMARY KEY AUTOINCREMENT,
"Enabled" INTEGER NOT NULL,
"Name" TEXT NOT NULL,
"DisplayName" TEXT NULL,
"Description" TEXT NULL,
"Required" INTEGER NOT NULL,
"Emphasize" INTEGER NOT NULL,
"ShowInDiscoveryDocument" INTEGER NOT NULL,
"Created" TEXT NOT NULL,
"Updated" TEXT NULL,
"LastAccessed" TEXT NULL,
"NonEditable" INTEGER NOT NULL
);
CREATE TABLE "Clients" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Clients" PRIMARY KEY AUTOINCREMENT,
"Enabled" INTEGER NOT NULL,
"ClientId" TEXT NOT NULL,
"ProtocolType" TEXT NOT NULL,
"RequireClientSecret" INTEGER NOT NULL,
"ClientName" TEXT NULL,
"Description" TEXT NULL,
"ClientUri" TEXT NULL,
"LogoUri" TEXT NULL,
"RequireConsent" INTEGER NOT NULL,
"AllowRememberConsent" INTEGER NOT NULL,
"AlwaysIncludeUserClaimsInIdToken" INTEGER NOT NULL,
"RequirePkce" INTEGER NOT NULL,
"AllowPlainTextPkce" INTEGER NOT NULL,
"RequireRequestObject" INTEGER NOT NULL,
"AllowAccessTokensViaBrowser" INTEGER NOT NULL,
"RequireDPoP" INTEGER NOT NULL,
"DPoPValidationMode" INTEGER NOT NULL,
"DPoPClockSkew" TEXT NOT NULL,
"FrontChannelLogoutUri" TEXT NULL,
"FrontChannelLogoutSessionRequired" INTEGER NOT NULL,
"BackChannelLogoutUri" TEXT NULL,
"BackChannelLogoutSessionRequired" INTEGER NOT NULL,
"AllowOfflineAccess" INTEGER NOT NULL,
"IdentityTokenLifetime" INTEGER NOT NULL,
"AllowedIdentityTokenSigningAlgorithms" TEXT NULL,
"AccessTokenLifetime" INTEGER NOT NULL,
"AuthorizationCodeLifetime" INTEGER NOT NULL,
"ConsentLifetime" INTEGER NULL,
"AbsoluteRefreshTokenLifetime" INTEGER NOT NULL,
"SlidingRefreshTokenLifetime" INTEGER NOT NULL,
"RefreshTokenUsage" INTEGER NOT NULL,
"UpdateAccessTokenClaimsOnRefresh" INTEGER NOT NULL,
"RefreshTokenExpiration" INTEGER NOT NULL,
"AccessTokenType" INTEGER NOT NULL,
"EnableLocalLogin" INTEGER NOT NULL,
"IncludeJwtId" INTEGER NOT NULL,
"AlwaysSendClientClaims" INTEGER NOT NULL,
"ClientClaimsPrefix" TEXT NULL,
"PairWiseSubjectSalt" TEXT NULL,
"InitiateLoginUri" TEXT NULL,
"UserSsoLifetime" INTEGER NULL,
"UserCodeType" TEXT NULL,
"DeviceCodeLifetime" INTEGER NOT NULL,
"CibaLifetime" INTEGER NULL,
"PollingInterval" INTEGER NULL,
"CoordinateLifetimeWithUserSession" INTEGER NULL,
"Created" TEXT NOT NULL,
"Updated" TEXT NULL,
"LastAccessed" TEXT NULL,
"NonEditable" INTEGER NOT NULL,
"PushedAuthorizationLifetime" INTEGER NULL,
"RequirePushedAuthorization" INTEGER NOT NULL
);
CREATE TABLE "IdentityProviders" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityProviders" PRIMARY KEY AUTOINCREMENT,
"Scheme" TEXT NOT NULL,
"DisplayName" TEXT NULL,
"Enabled" INTEGER NOT NULL,
"Type" TEXT NOT NULL,
"Properties" TEXT NULL,
"Created" TEXT NOT NULL,
"Updated" TEXT NULL,
"LastAccessed" TEXT NULL,
"NonEditable" INTEGER NOT NULL
);
CREATE TABLE "IdentityResources" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResources" PRIMARY KEY AUTOINCREMENT,
"Enabled" INTEGER NOT NULL,
"Name" TEXT NOT NULL,
"DisplayName" TEXT NULL,
"Description" TEXT NULL,
"Required" INTEGER NOT NULL,
"Emphasize" INTEGER NOT NULL,
"ShowInDiscoveryDocument" INTEGER NOT NULL,
"Created" TEXT NOT NULL,
"Updated" TEXT NULL,
"NonEditable" INTEGER NOT NULL
);
CREATE TABLE "ApiResourceClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceClaims" PRIMARY KEY AUTOINCREMENT,
"ApiResourceId" INTEGER NOT NULL,
"Type" TEXT NOT NULL,
CONSTRAINT "FK_ApiResourceClaims_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ApiResourceProperties" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceProperties" PRIMARY KEY AUTOINCREMENT,
"ApiResourceId" INTEGER NOT NULL,
"Key" TEXT NOT NULL,
"Value" TEXT NOT NULL,
CONSTRAINT "FK_ApiResourceProperties_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ApiResourceScopes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceScopes" PRIMARY KEY AUTOINCREMENT,
"Scope" TEXT NOT NULL,
"ApiResourceId" INTEGER NOT NULL,
CONSTRAINT "FK_ApiResourceScopes_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ApiResourceSecrets" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiResourceSecrets" PRIMARY KEY AUTOINCREMENT,
"ApiResourceId" INTEGER NOT NULL,
"Description" TEXT NULL,
"Value" TEXT NOT NULL,
"Expiration" TEXT NULL,
"Type" TEXT NOT NULL,
"Created" TEXT NOT NULL,
CONSTRAINT "FK_ApiResourceSecrets_ApiResources_ApiResourceId" FOREIGN KEY ("ApiResourceId") REFERENCES "ApiResources" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ApiScopeClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeClaims" PRIMARY KEY AUTOINCREMENT,
"ScopeId" INTEGER NOT NULL,
"Type" TEXT NOT NULL,
CONSTRAINT "FK_ApiScopeClaims_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ApiScopeProperties" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ApiScopeProperties" PRIMARY KEY AUTOINCREMENT,
"ScopeId" INTEGER NOT NULL,
"Key" TEXT NOT NULL,
"Value" TEXT NOT NULL,
CONSTRAINT "FK_ApiScopeProperties_ApiScopes_ScopeId" FOREIGN KEY ("ScopeId") REFERENCES "ApiScopes" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientClaims" PRIMARY KEY AUTOINCREMENT,
"Type" TEXT NOT NULL,
"Value" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientClaims_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientCorsOrigins" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientCorsOrigins" PRIMARY KEY AUTOINCREMENT,
"Origin" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientCorsOrigins_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientGrantTypes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientGrantTypes" PRIMARY KEY AUTOINCREMENT,
"GrantType" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientGrantTypes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientIdPRestrictions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientIdPRestrictions" PRIMARY KEY AUTOINCREMENT,
"Provider" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientIdPRestrictions_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientPostLogoutRedirectUris" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientPostLogoutRedirectUris" PRIMARY KEY AUTOINCREMENT,
"PostLogoutRedirectUri" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientPostLogoutRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientProperties" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientProperties" PRIMARY KEY AUTOINCREMENT,
"ClientId" INTEGER NOT NULL,
"Key" TEXT NOT NULL,
"Value" TEXT NOT NULL,
CONSTRAINT "FK_ClientProperties_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientRedirectUris" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientRedirectUris" PRIMARY KEY AUTOINCREMENT,
"RedirectUri" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientRedirectUris_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientScopes" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientScopes" PRIMARY KEY AUTOINCREMENT,
"Scope" TEXT NOT NULL,
"ClientId" INTEGER NOT NULL,
CONSTRAINT "FK_ClientScopes_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "ClientSecrets" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClientSecrets" PRIMARY KEY AUTOINCREMENT,
"ClientId" INTEGER NOT NULL,
"Description" TEXT NULL,
"Value" TEXT NOT NULL,
"Expiration" TEXT NULL,
"Type" TEXT NOT NULL,
"Created" TEXT NOT NULL,
CONSTRAINT "FK_ClientSecrets_Clients_ClientId" FOREIGN KEY ("ClientId") REFERENCES "Clients" ("Id") ON DELETE CASCADE
);
CREATE TABLE "IdentityResourceClaims" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceClaims" PRIMARY KEY AUTOINCREMENT,
"IdentityResourceId" INTEGER NOT NULL,
"Type" TEXT NOT NULL,
CONSTRAINT "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
);
CREATE TABLE "IdentityResourceProperties" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_IdentityResourceProperties" PRIMARY KEY AUTOINCREMENT,
"IdentityResourceId" INTEGER NOT NULL,
"Key" TEXT NOT NULL,
"Value" TEXT NOT NULL,
CONSTRAINT "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId" FOREIGN KEY ("IdentityResourceId") REFERENCES "IdentityResources" ("Id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX "IX_ApiResourceClaims_ApiResourceId_Type" ON "ApiResourceClaims" ("ApiResourceId", "Type");
CREATE UNIQUE INDEX "IX_ApiResourceProperties_ApiResourceId_Key" ON "ApiResourceProperties" ("ApiResourceId", "Key");
CREATE UNIQUE INDEX "IX_ApiResources_Name" ON "ApiResources" ("Name");
CREATE UNIQUE INDEX "IX_ApiResourceScopes_ApiResourceId_Scope" ON "ApiResourceScopes" ("ApiResourceId", "Scope");
CREATE INDEX "IX_ApiResourceSecrets_ApiResourceId" ON "ApiResourceSecrets" ("ApiResourceId");
CREATE UNIQUE INDEX "IX_ApiScopeClaims_ScopeId_Type" ON "ApiScopeClaims" ("ScopeId", "Type");
CREATE UNIQUE INDEX "IX_ApiScopeProperties_ScopeId_Key" ON "ApiScopeProperties" ("ScopeId", "Key");
CREATE UNIQUE INDEX "IX_ApiScopes_Name" ON "ApiScopes" ("Name");
CREATE UNIQUE INDEX "IX_ClientClaims_ClientId_Type_Value" ON "ClientClaims" ("ClientId", "Type", "Value");
CREATE UNIQUE INDEX "IX_ClientCorsOrigins_ClientId_Origin" ON "ClientCorsOrigins" ("ClientId", "Origin");
CREATE UNIQUE INDEX "IX_ClientGrantTypes_ClientId_GrantType" ON "ClientGrantTypes" ("ClientId", "GrantType");
CREATE UNIQUE INDEX "IX_ClientIdPRestrictions_ClientId_Provider" ON "ClientIdPRestrictions" ("ClientId", "Provider");
CREATE UNIQUE INDEX "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri" ON "ClientPostLogoutRedirectUris" ("ClientId", "PostLogoutRedirectUri");
CREATE UNIQUE INDEX "IX_ClientProperties_ClientId_Key" ON "ClientProperties" ("ClientId", "Key");
CREATE UNIQUE INDEX "IX_ClientRedirectUris_ClientId_RedirectUri" ON "ClientRedirectUris" ("ClientId", "RedirectUri");
CREATE UNIQUE INDEX "IX_Clients_ClientId" ON "Clients" ("ClientId");
CREATE UNIQUE INDEX "IX_ClientScopes_ClientId_Scope" ON "ClientScopes" ("ClientId", "Scope");
CREATE INDEX "IX_ClientSecrets_ClientId" ON "ClientSecrets" ("ClientId");
CREATE UNIQUE INDEX "IX_IdentityProviders_Scheme" ON "IdentityProviders" ("Scheme");
CREATE UNIQUE INDEX "IX_IdentityResourceClaims_IdentityResourceId_Type" ON "IdentityResourceClaims" ("IdentityResourceId", "Type");
CREATE UNIQUE INDEX "IX_IdentityResourceProperties_IdentityResourceId_Key" ON "IdentityResourceProperties" ("IdentityResourceId", "Key");
CREATE UNIQUE INDEX "IX_IdentityResources_Name" ON "IdentityResources" ("Name");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20240312131641_Configuration', '8.0.0');
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,721 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Microser.IdS.Migrations.ConfigurationDb
{
/// <inheritdoc />
public partial class Configuration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiResources",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
AllowedAccessTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
RequireResourceIndicator = table.Column<bool>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResources", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ApiScopes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
Required = table.Column<bool>(type: "INTEGER", nullable: false),
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Clients",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
ProtocolType = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
RequireClientSecret = table.Column<bool>(type: "INTEGER", nullable: false),
ClientName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
ClientUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
LogoUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
RequireConsent = table.Column<bool>(type: "INTEGER", nullable: false),
AllowRememberConsent = table.Column<bool>(type: "INTEGER", nullable: false),
AlwaysIncludeUserClaimsInIdToken = table.Column<bool>(type: "INTEGER", nullable: false),
RequirePkce = table.Column<bool>(type: "INTEGER", nullable: false),
AllowPlainTextPkce = table.Column<bool>(type: "INTEGER", nullable: false),
RequireRequestObject = table.Column<bool>(type: "INTEGER", nullable: false),
AllowAccessTokensViaBrowser = table.Column<bool>(type: "INTEGER", nullable: false),
RequireDPoP = table.Column<bool>(type: "INTEGER", nullable: false),
DPoPValidationMode = table.Column<int>(type: "INTEGER", nullable: false),
DPoPClockSkew = table.Column<TimeSpan>(type: "TEXT", nullable: false),
FrontChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
FrontChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
BackChannelLogoutUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
BackChannelLogoutSessionRequired = table.Column<bool>(type: "INTEGER", nullable: false),
AllowOfflineAccess = table.Column<bool>(type: "INTEGER", nullable: false),
IdentityTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
AllowedIdentityTokenSigningAlgorithms = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
AccessTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
AuthorizationCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
ConsentLifetime = table.Column<int>(type: "INTEGER", nullable: true),
AbsoluteRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
SlidingRefreshTokenLifetime = table.Column<int>(type: "INTEGER", nullable: false),
RefreshTokenUsage = table.Column<int>(type: "INTEGER", nullable: false),
UpdateAccessTokenClaimsOnRefresh = table.Column<bool>(type: "INTEGER", nullable: false),
RefreshTokenExpiration = table.Column<int>(type: "INTEGER", nullable: false),
AccessTokenType = table.Column<int>(type: "INTEGER", nullable: false),
EnableLocalLogin = table.Column<bool>(type: "INTEGER", nullable: false),
IncludeJwtId = table.Column<bool>(type: "INTEGER", nullable: false),
AlwaysSendClientClaims = table.Column<bool>(type: "INTEGER", nullable: false),
ClientClaimsPrefix = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
PairWiseSubjectSalt = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
InitiateLoginUri = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
UserSsoLifetime = table.Column<int>(type: "INTEGER", nullable: true),
UserCodeType = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
DeviceCodeLifetime = table.Column<int>(type: "INTEGER", nullable: false),
CibaLifetime = table.Column<int>(type: "INTEGER", nullable: true),
PollingInterval = table.Column<int>(type: "INTEGER", nullable: true),
CoordinateLifetimeWithUserSession = table.Column<bool>(type: "INTEGER", nullable: true),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false),
PushedAuthorizationLifetime = table.Column<int>(type: "INTEGER", nullable: true),
RequirePushedAuthorization = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Clients", x => x.Id);
});
migrationBuilder.CreateTable(
name: "IdentityProviders",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Scheme = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
Properties = table.Column<string>(type: "TEXT", nullable: true),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
LastAccessed = table.Column<DateTime>(type: "TEXT", nullable: true),
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityProviders", x => x.Id);
});
migrationBuilder.CreateTable(
name: "IdentityResources",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DisplayName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
Required = table.Column<bool>(type: "INTEGER", nullable: false),
Emphasize = table.Column<bool>(type: "INTEGER", nullable: false),
ShowInDiscoveryDocument = table.Column<bool>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Updated = table.Column<DateTime>(type: "TEXT", nullable: true),
NonEditable = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResources", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ApiResourceClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceClaims_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceProperties",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceProperties", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceProperties_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceScopes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceScopes", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceScopes_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiResourceSecrets",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApiResourceId = table.Column<int>(type: "INTEGER", nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiResourceSecrets", x => x.Id);
table.ForeignKey(
name: "FK_ApiResourceSecrets_ApiResources_ApiResourceId",
column: x => x.ApiResourceId,
principalTable: "ApiResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiScopeClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeClaims", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeClaims_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApiScopeProperties",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ScopeId = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiScopeProperties", x => x.Id);
table.ForeignKey(
name: "FK_ApiScopeProperties_ApiScopes_ScopeId",
column: x => x.ScopeId,
principalTable: "ApiScopes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientClaims", x => x.Id);
table.ForeignKey(
name: "FK_ClientClaims_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientCorsOrigins",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Origin = table.Column<string>(type: "TEXT", maxLength: 150, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id);
table.ForeignKey(
name: "FK_ClientCorsOrigins_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientGrantTypes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GrantType = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientGrantTypes", x => x.Id);
table.ForeignKey(
name: "FK_ClientGrantTypes_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientIdPRestrictions",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Provider = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id);
table.ForeignKey(
name: "FK_ClientIdPRestrictions_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientPostLogoutRedirectUris",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PostLogoutRedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id);
table.ForeignKey(
name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientProperties",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientProperties", x => x.Id);
table.ForeignKey(
name: "FK_ClientProperties_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientRedirectUris",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
RedirectUri = table.Column<string>(type: "TEXT", maxLength: 400, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientRedirectUris", x => x.Id);
table.ForeignKey(
name: "FK_ClientRedirectUris_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientScopes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Scope = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
ClientId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientScopes", x => x.Id);
table.ForeignKey(
name: "FK_ClientScopes_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ClientSecrets",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ClientId = table.Column<int>(type: "INTEGER", nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
Value = table.Column<string>(type: "TEXT", maxLength: 4000, nullable: false),
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ClientSecrets", x => x.Id);
table.ForeignKey(
name: "FK_ClientSecrets_Clients_ClientId",
column: x => x.ClientId,
principalTable: "Clients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IdentityResourceClaims",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResourceClaims", x => x.Id);
table.ForeignKey(
name: "FK_IdentityResourceClaims_IdentityResources_IdentityResourceId",
column: x => x.IdentityResourceId,
principalTable: "IdentityResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IdentityResourceProperties",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
IdentityResourceId = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", maxLength: 250, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IdentityResourceProperties", x => x.Id);
table.ForeignKey(
name: "FK_IdentityResourceProperties_IdentityResources_IdentityResourceId",
column: x => x.IdentityResourceId,
principalTable: "IdentityResources",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ApiResourceClaims_ApiResourceId_Type",
table: "ApiResourceClaims",
columns: new[] { "ApiResourceId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiResourceProperties_ApiResourceId_Key",
table: "ApiResourceProperties",
columns: new[] { "ApiResourceId", "Key" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiResources_Name",
table: "ApiResources",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiResourceScopes_ApiResourceId_Scope",
table: "ApiResourceScopes",
columns: new[] { "ApiResourceId", "Scope" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiResourceSecrets_ApiResourceId",
table: "ApiResourceSecrets",
column: "ApiResourceId");
migrationBuilder.CreateIndex(
name: "IX_ApiScopeClaims_ScopeId_Type",
table: "ApiScopeClaims",
columns: new[] { "ScopeId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiScopeProperties_ScopeId_Key",
table: "ApiScopeProperties",
columns: new[] { "ScopeId", "Key" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ApiScopes_Name",
table: "ApiScopes",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientClaims_ClientId_Type_Value",
table: "ClientClaims",
columns: new[] { "ClientId", "Type", "Value" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientCorsOrigins_ClientId_Origin",
table: "ClientCorsOrigins",
columns: new[] { "ClientId", "Origin" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientGrantTypes_ClientId_GrantType",
table: "ClientGrantTypes",
columns: new[] { "ClientId", "GrantType" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientIdPRestrictions_ClientId_Provider",
table: "ClientIdPRestrictions",
columns: new[] { "ClientId", "Provider" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientPostLogoutRedirectUris_ClientId_PostLogoutRedirectUri",
table: "ClientPostLogoutRedirectUris",
columns: new[] { "ClientId", "PostLogoutRedirectUri" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientProperties_ClientId_Key",
table: "ClientProperties",
columns: new[] { "ClientId", "Key" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientRedirectUris_ClientId_RedirectUri",
table: "ClientRedirectUris",
columns: new[] { "ClientId", "RedirectUri" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Clients_ClientId",
table: "Clients",
column: "ClientId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientScopes_ClientId_Scope",
table: "ClientScopes",
columns: new[] { "ClientId", "Scope" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ClientSecrets_ClientId",
table: "ClientSecrets",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_IdentityProviders_Scheme",
table: "IdentityProviders",
column: "Scheme",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_IdentityResourceClaims_IdentityResourceId_Type",
table: "IdentityResourceClaims",
columns: new[] { "IdentityResourceId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_IdentityResourceProperties_IdentityResourceId_Key",
table: "IdentityResourceProperties",
columns: new[] { "IdentityResourceId", "Key" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_IdentityResources_Name",
table: "IdentityResources",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiResourceClaims");
migrationBuilder.DropTable(
name: "ApiResourceProperties");
migrationBuilder.DropTable(
name: "ApiResourceScopes");
migrationBuilder.DropTable(
name: "ApiResourceSecrets");
migrationBuilder.DropTable(
name: "ApiScopeClaims");
migrationBuilder.DropTable(
name: "ApiScopeProperties");
migrationBuilder.DropTable(
name: "ClientClaims");
migrationBuilder.DropTable(
name: "ClientCorsOrigins");
migrationBuilder.DropTable(
name: "ClientGrantTypes");
migrationBuilder.DropTable(
name: "ClientIdPRestrictions");
migrationBuilder.DropTable(
name: "ClientPostLogoutRedirectUris");
migrationBuilder.DropTable(
name: "ClientProperties");
migrationBuilder.DropTable(
name: "ClientRedirectUris");
migrationBuilder.DropTable(
name: "ClientScopes");
migrationBuilder.DropTable(
name: "ClientSecrets");
migrationBuilder.DropTable(
name: "IdentityProviders");
migrationBuilder.DropTable(
name: "IdentityResourceClaims");
migrationBuilder.DropTable(
name: "IdentityResourceProperties");
migrationBuilder.DropTable(
name: "ApiResources");
migrationBuilder.DropTable(
name: "ApiScopes");
migrationBuilder.DropTable(
name: "Clients");
migrationBuilder.DropTable(
name: "IdentityResources");
}
}
}

View File

@@ -0,0 +1,99 @@
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
BEGIN TRANSACTION;
CREATE TABLE "DeviceCodes" (
"UserCode" TEXT NOT NULL CONSTRAINT "PK_DeviceCodes" PRIMARY KEY,
"DeviceCode" TEXT NOT NULL,
"SubjectId" TEXT NULL,
"SessionId" TEXT NULL,
"ClientId" TEXT NOT NULL,
"Description" TEXT NULL,
"CreationTime" TEXT NOT NULL,
"Expiration" TEXT NOT NULL,
"Data" TEXT NOT NULL
);
CREATE TABLE "Keys" (
"Id" TEXT NOT NULL CONSTRAINT "PK_Keys" PRIMARY KEY,
"Version" INTEGER NOT NULL,
"Created" TEXT NOT NULL,
"Use" TEXT NULL,
"Algorithm" TEXT NOT NULL,
"IsX509Certificate" INTEGER NOT NULL,
"DataProtected" INTEGER NOT NULL,
"Data" TEXT NOT NULL
);
CREATE TABLE "PersistedGrants" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PersistedGrants" PRIMARY KEY AUTOINCREMENT,
"Key" TEXT NULL,
"Type" TEXT NOT NULL,
"SubjectId" TEXT NULL,
"SessionId" TEXT NULL,
"ClientId" TEXT NOT NULL,
"Description" TEXT NULL,
"CreationTime" TEXT NOT NULL,
"Expiration" TEXT NULL,
"ConsumedTime" TEXT NULL,
"Data" TEXT NOT NULL
);
CREATE TABLE "PushedAuthorizationRequests" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_PushedAuthorizationRequests" PRIMARY KEY AUTOINCREMENT,
"ReferenceValueHash" TEXT NOT NULL,
"ExpiresAtUtc" TEXT NOT NULL,
"Parameters" TEXT NOT NULL
);
CREATE TABLE "ServerSideSessions" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_ServerSideSessions" PRIMARY KEY AUTOINCREMENT,
"Key" TEXT NOT NULL,
"Scheme" TEXT NOT NULL,
"SubjectId" TEXT NOT NULL,
"SessionId" TEXT NULL,
"DisplayName" TEXT NULL,
"Created" TEXT NOT NULL,
"Renewed" TEXT NOT NULL,
"Expires" TEXT NULL,
"Data" TEXT NOT NULL
);
CREATE UNIQUE INDEX "IX_DeviceCodes_DeviceCode" ON "DeviceCodes" ("DeviceCode");
CREATE INDEX "IX_DeviceCodes_Expiration" ON "DeviceCodes" ("Expiration");
CREATE INDEX "IX_Keys_Use" ON "Keys" ("Use");
CREATE INDEX "IX_PersistedGrants_ConsumedTime" ON "PersistedGrants" ("ConsumedTime");
CREATE INDEX "IX_PersistedGrants_Expiration" ON "PersistedGrants" ("Expiration");
CREATE UNIQUE INDEX "IX_PersistedGrants_Key" ON "PersistedGrants" ("Key");
CREATE INDEX "IX_PersistedGrants_SubjectId_ClientId_Type" ON "PersistedGrants" ("SubjectId", "ClientId", "Type");
CREATE INDEX "IX_PersistedGrants_SubjectId_SessionId_Type" ON "PersistedGrants" ("SubjectId", "SessionId", "Type");
CREATE INDEX "IX_PushedAuthorizationRequests_ExpiresAtUtc" ON "PushedAuthorizationRequests" ("ExpiresAtUtc");
CREATE UNIQUE INDEX "IX_PushedAuthorizationRequests_ReferenceValueHash" ON "PushedAuthorizationRequests" ("ReferenceValueHash");
CREATE INDEX "IX_ServerSideSessions_DisplayName" ON "ServerSideSessions" ("DisplayName");
CREATE INDEX "IX_ServerSideSessions_Expires" ON "ServerSideSessions" ("Expires");
CREATE UNIQUE INDEX "IX_ServerSideSessions_Key" ON "ServerSideSessions" ("Key");
CREATE INDEX "IX_ServerSideSessions_SessionId" ON "ServerSideSessions" ("SessionId");
CREATE INDEX "IX_ServerSideSessions_SubjectId" ON "ServerSideSessions" ("SubjectId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20240312131625_Grants', '8.0.0');
COMMIT;

View File

@@ -0,0 +1,259 @@
// <auto-generated />
using System;
using Duende.IdentityServer.EntityFramework.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Microser.IdS.Migrations.PersistedGrantDb
{
[DbContext(typeof(PersistedGrantDbContext))]
[Migration("20240312131625_Grants")]
partial class Grants
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
{
b.Property<string>("UserCode")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("UserCode");
b.HasIndex("DeviceCode")
.IsUnique();
b.HasIndex("Expiration");
b.ToTable("DeviceCodes", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Algorithm")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DataProtected")
.HasColumnType("INTEGER");
b.Property<bool>("IsX509Certificate")
.HasColumnType("INTEGER");
b.Property<string>("Use")
.HasColumnType("TEXT");
b.Property<int>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Use");
b.ToTable("Keys", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ConsumedTime");
b.HasIndex("Expiration");
b.HasIndex("Key")
.IsUnique();
b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("ExpiresAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Parameters")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ReferenceValueHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ExpiresAtUtc");
b.HasIndex("ReferenceValueHash")
.IsUnique();
b.ToTable("PushedAuthorizationRequests", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expires")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Renewed")
.HasColumnType("TEXT");
b.Property<string>("Scheme")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DisplayName");
b.HasIndex("Expires");
b.HasIndex("Key")
.IsUnique();
b.HasIndex("SessionId");
b.HasIndex("SubjectId");
b.ToTable("ServerSideSessions", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,207 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Microser.IdS.Migrations.PersistedGrantDb
{
/// <inheritdoc />
public partial class Grants : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DeviceCodes",
columns: table => new
{
UserCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
DeviceCode = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
Expiration = table.Column<DateTime>(type: "TEXT", nullable: false),
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceCodes", x => x.UserCode);
});
migrationBuilder.CreateTable(
name: "Keys",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Version = table.Column<int>(type: "INTEGER", nullable: false),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Use = table.Column<string>(type: "TEXT", nullable: true),
Algorithm = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
IsX509Certificate = table.Column<bool>(type: "INTEGER", nullable: false),
DataProtected = table.Column<bool>(type: "INTEGER", nullable: false),
Data = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Keys", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PersistedGrants",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
SubjectId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ClientId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false),
Expiration = table.Column<DateTime>(type: "TEXT", nullable: true),
ConsumedTime = table.Column<DateTime>(type: "TEXT", nullable: true),
Data = table.Column<string>(type: "TEXT", maxLength: 50000, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PersistedGrants", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PushedAuthorizationRequests",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ReferenceValueHash = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
Parameters = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PushedAuthorizationRequests", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ServerSideSessions",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Key = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Scheme = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
SubjectId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
SessionId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
DisplayName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
Renewed = table.Column<DateTime>(type: "TEXT", nullable: false),
Expires = table.Column<DateTime>(type: "TEXT", nullable: true),
Data = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerSideSessions", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_DeviceCodes_DeviceCode",
table: "DeviceCodes",
column: "DeviceCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DeviceCodes_Expiration",
table: "DeviceCodes",
column: "Expiration");
migrationBuilder.CreateIndex(
name: "IX_Keys_Use",
table: "Keys",
column: "Use");
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_ConsumedTime",
table: "PersistedGrants",
column: "ConsumedTime");
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_Expiration",
table: "PersistedGrants",
column: "Expiration");
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_Key",
table: "PersistedGrants",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_SubjectId_ClientId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "ClientId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "SessionId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_PushedAuthorizationRequests_ExpiresAtUtc",
table: "PushedAuthorizationRequests",
column: "ExpiresAtUtc");
migrationBuilder.CreateIndex(
name: "IX_PushedAuthorizationRequests_ReferenceValueHash",
table: "PushedAuthorizationRequests",
column: "ReferenceValueHash",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ServerSideSessions_DisplayName",
table: "ServerSideSessions",
column: "DisplayName");
migrationBuilder.CreateIndex(
name: "IX_ServerSideSessions_Expires",
table: "ServerSideSessions",
column: "Expires");
migrationBuilder.CreateIndex(
name: "IX_ServerSideSessions_Key",
table: "ServerSideSessions",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ServerSideSessions_SessionId",
table: "ServerSideSessions",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_ServerSideSessions_SubjectId",
table: "ServerSideSessions",
column: "SubjectId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeviceCodes");
migrationBuilder.DropTable(
name: "Keys");
migrationBuilder.DropTable(
name: "PersistedGrants");
migrationBuilder.DropTable(
name: "PushedAuthorizationRequests");
migrationBuilder.DropTable(
name: "ServerSideSessions");
}
}
}

View File

@@ -0,0 +1,256 @@
// <auto-generated />
using System;
using Duende.IdentityServer.EntityFramework.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Microser.IdS.Migrations.PersistedGrantDb
{
[DbContext(typeof(PersistedGrantDbContext))]
partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b =>
{
b.Property<string>("UserCode")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("UserCode");
b.HasIndex("DeviceCode")
.IsUnique();
b.HasIndex("Expiration");
b.ToTable("DeviceCodes", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Algorithm")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("DataProtected")
.HasColumnType("INTEGER");
b.Property<bool>("IsX509Certificate")
.HasColumnType("INTEGER");
b.Property<string>("Use")
.HasColumnType("TEXT");
b.Property<int>("Version")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Use");
b.ToTable("Keys", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("ConsumedTime")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationTime")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expiration")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ConsumedTime");
b.HasIndex("Expiration");
b.HasIndex("Key")
.IsUnique();
b.HasIndex("SubjectId", "ClientId", "Type");
b.HasIndex("SubjectId", "SessionId", "Type");
b.ToTable("PersistedGrants", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PushedAuthorizationRequest", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("ExpiresAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Parameters")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ReferenceValueHash")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ExpiresAtUtc");
b.HasIndex("ReferenceValueHash")
.IsUnique();
b.ToTable("PushedAuthorizationRequests", (string)null);
});
modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.ServerSideSession", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime?>("Expires")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Renewed")
.HasColumnType("TEXT");
b.Property<string>("Scheme")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SessionId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SubjectId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DisplayName");
b.HasIndex("Expires");
b.HasIndex("Key")
.IsUnique();
b.HasIndex("SessionId");
b.HasIndex("SubjectId");
b.ToTable("ServerSideSessions", (string)null);
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,10 @@
@page
@model Microser.IdS.Pages.Account.AccessDeniedModel
@{
}
<div class="row">
<div class="col">
<h1>Access Denied</h1>
<p>You do not have permission to access that resource.</p>
</div>
</div>

View File

@@ -0,0 +1,14 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Account
{
public class AccessDeniedModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,40 @@
@page
@model Microser.IdS.Pages.Create.Index
<div class="login-page">
<div class="lead">
<h1>Create Account</h1>
</div>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-sm-6">
<form asp-page="/Account/Create/Index">
<input type="hidden" asp-for="Input.ReturnUrl" />
<div class="form-group">
<label asp-for="Input.Username"></label>
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
</div>
<div class="form-group">
<label asp-for="Input.Name"></label>
<input type="text" class="form-control" placeholder="Name" asp-for="Input.Name">
</div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input type="email" class="form-control" placeholder="Email" asp-for="Input.Email" >
</div>
<button class="btn btn-primary" name="Input.Button" value="create">Create</button>
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,122 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Test;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Create
{
[SecurityHeaders]
[AllowAnonymous]
public class Index : PageModel
{
private readonly TestUserStore _users;
private readonly IIdentityServerInteractionService _interaction;
[BindProperty]
public InputModel Input { get; set; } = default!;
public Index(
IIdentityServerInteractionService interaction,
TestUserStore? users = null)
{
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
_interaction = interaction;
}
public IActionResult OnGet(string? returnUrl)
{
Input = new InputModel { ReturnUrl = returnUrl };
return Page();
}
public async Task<IActionResult> OnPost()
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
// the user clicked the "cancel" button
if (Input.Button != "create")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage(Input.ReturnUrl);
}
return Redirect(Input.ReturnUrl ?? "~/");
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (_users.FindByUsername(Input.Username) != null)
{
ModelState.AddModelError("Input.Username", "Invalid username");
}
if (ModelState.IsValid)
{
var user = _users.CreateUser(Input.Username, Input.Password, Input.Name, Input.Email);
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(isuser);
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage(Input.ReturnUrl);
}
// we can trust Input.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(Input.ReturnUrl ?? "~/");
}
// request for a local page
if (Url.IsLocalUrl(Input.ReturnUrl))
{
return Redirect(Input.ReturnUrl);
}
else if (string.IsNullOrEmpty(Input.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new ArgumentException("invalid return URL");
}
}
return Page();
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace Microser.IdS.Pages.Create
{
public class InputModel
{
[Required]
public string? Username { get; set; }
[Required]
public string? Password { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string? ReturnUrl { get; set; }
public string? Button { get; set; }
}
}

View File

@@ -0,0 +1,89 @@
@page
@model Microser.IdS.Pages.Login.Index
<div class="login-page">
<div class="lead">
<h1>Login</h1>
<p>Choose how to login</p>
</div>
<partial name="_ValidationSummary" />
<div class="row">
@if (Model.View.EnableLocalLogin)
{
<div class="col-sm-6">
<div class="card">
<div class="card-header">
<h2>Local Account</h2>
</div>
<div class="card-body">
<form asp-page="/Account/Login/Index">
<input type="hidden" asp-for="Input.ReturnUrl" />
<div class="form-group">
<label asp-for="Input.Username"></label>
<input class="form-control" placeholder="Username" asp-for="Input.Username" autofocus>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input type="password" class="form-control" placeholder="Password" asp-for="Input.Password" autocomplete="off">
</div>
@if (Model.View.AllowRememberLogin)
{
<div class="form-group">
<div class="form-check">
<input class="form-check-input" asp-for="Input.RememberLogin">
<label class="form-check-label" asp-for="Input.RememberLogin">
Remember My Login
</label>
</div>
</div>
}
<button class="btn btn-primary" name="Input.Button" value="login">Login</button>
<button class="btn btn-secondary" name="Input.Button" value="cancel">Cancel</button>
</form>
</div>
</div>
</div>
}
@if (Model.View.VisibleExternalProviders.Any())
{
<div class="col-sm-6">
<div class="card">
<div class="card-header">
<h2>External Account</h2>
</div>
<div class="card-body">
<ul class="list-inline">
@foreach (var provider in Model.View.VisibleExternalProviders)
{
<li class="list-inline-item">
<a class="btn btn-secondary"
asp-page="/ExternalLogin/Challenge"
asp-route-scheme="@provider.AuthenticationScheme"
asp-route-returnUrl="@Model.Input.ReturnUrl">
@provider.DisplayName
</a>
</li>
}
</ul>
</div>
</div>
</div>
}
@if (!Model.View.EnableLocalLogin && !Model.View.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>Invalid login request</strong>
There are no login schemes configured for this request.
</div>
}
</div>
</div>

View File

@@ -0,0 +1,231 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using Duende.IdentityServer.Test;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Login
{
[SecurityHeaders]
[AllowAnonymous]
public class Index : PageModel
{
private readonly TestUserStore _users;
private readonly IIdentityServerInteractionService _interaction;
private readonly IEventService _events;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IIdentityProviderStore _identityProviderStore;
public ViewModel View { get; set; } = default!;
[BindProperty]
public InputModel Input { get; set; } = default!;
public Index(
IIdentityServerInteractionService interaction,
IAuthenticationSchemeProvider schemeProvider,
IIdentityProviderStore identityProviderStore,
IEventService events,
TestUserStore? users = null)
{
// this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity)
_users = users ?? throw new InvalidOperationException("Please call 'AddTestUsers(TestUsers.Users)' on the IIdentityServerBuilder in Startup or remove the TestUserStore from the AccountController.");
_interaction = interaction;
_schemeProvider = schemeProvider;
_identityProviderStore = identityProviderStore;
_events = events;
}
public async Task<IActionResult> OnGet(string? returnUrl)
{
await BuildModelAsync(returnUrl);
if (View.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToPage("/ExternalLogin/Challenge", new { scheme = View.ExternalLoginScheme, returnUrl });
}
return Page();
}
public async Task<IActionResult> OnPost()
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(Input.ReturnUrl);
// the user clicked the "cancel" button
if (Input.Button != "login")
{
if (context != null)
{
// This "can't happen", because if the ReturnUrl was null, then the context would be null
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage(Input.ReturnUrl);
}
return Redirect(Input.ReturnUrl ?? "~/");
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (_users.ValidateCredentials(Input.Username, Input.Password))
{
var user = _users.FindByUsername(Input.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
var props = new AuthenticationProperties();
if (LoginOptions.AllowRememberLogin && Input.RememberLogin)
{
props.IsPersistent = true;
props.ExpiresUtc = DateTimeOffset.UtcNow.Add(LoginOptions.RememberMeLoginDuration);
};
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(isuser, props);
if (context != null)
{
// This "can't happen", because if the ReturnUrl was null, then the context would be null
ArgumentNullException.ThrowIfNull(Input.ReturnUrl, nameof(Input.ReturnUrl));
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage(Input.ReturnUrl);
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(Input.ReturnUrl ?? "~/");
}
// request for a local page
if (Url.IsLocalUrl(Input.ReturnUrl))
{
return Redirect(Input.ReturnUrl);
}
else if (string.IsNullOrEmpty(Input.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new ArgumentException("invalid return URL");
}
}
const string error = "invalid credentials";
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId: context?.Client.ClientId));
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
await BuildModelAsync(Input.ReturnUrl);
return Page();
}
private async Task BuildModelAsync(string? returnUrl)
{
Input = new InputModel
{
ReturnUrl = returnUrl
};
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
var local = context.IdP == Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider;
// this is meant to short circuit the UI and only trigger the one external IdP
View = new ViewModel
{
EnableLocalLogin = local,
};
Input.Username = context.LoginHint;
if (!local)
{
View.ExternalProviders = new[] { new ViewModel.ExternalProvider(authenticationScheme: context.IdP) };
}
return;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null)
.Select(x => new ViewModel.ExternalProvider
(
authenticationScheme: x.Name,
displayName: x.DisplayName ?? x.Name
)).ToList();
var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())
.Where(x => x.Enabled)
.Select(x => new ViewModel.ExternalProvider
(
authenticationScheme: x.Scheme,
displayName: x.DisplayName ?? x.Scheme
));
providers.AddRange(dynamicSchemes);
var allowLocal = true;
var client = context?.Client;
if (client != null)
{
allowLocal = client.EnableLocalLogin;
if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Count != 0)
{
providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();
}
}
View = new ViewModel
{
AllowRememberLogin = LoginOptions.AllowRememberLogin,
EnableLocalLogin = allowLocal && LoginOptions.AllowLocalLogin,
ExternalProviders = providers.ToArray()
};
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.ComponentModel.DataAnnotations;
namespace Microser.IdS.Pages.Login
{
public class InputModel
{
[Required]
public string? Username { get; set; }
[Required]
public string? Password { get; set; }
public bool RememberLogin { get; set; }
public string? ReturnUrl { get; set; }
public string? Button { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Login
{
public static class LoginOptions
{
public static readonly bool AllowLocalLogin = true;
public static readonly bool AllowRememberLogin = true;
public static readonly TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);
public static readonly string InvalidCredentialsErrorMessage = "Invalid username or password";
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Login
{
public class ViewModel
{
public bool AllowRememberLogin { get; set; } = true;
public bool EnableLocalLogin { get; set; } = true;
public IEnumerable<ViewModel.ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();
public IEnumerable<ViewModel.ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName));
public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;
public string? ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;
public class ExternalProvider
{
public ExternalProvider(string authenticationScheme, string? displayName = null)
{
AuthenticationScheme = authenticationScheme;
DisplayName = displayName;
}
public string? DisplayName { get; set; }
public string AuthenticationScheme { get; set; }
}
}
}

View File

@@ -0,0 +1,17 @@
@page
@model Microser.IdS.Pages.Logout.Index
<div class="logout-page">
<div class="lead">
<h1>Logout</h1>
<p>Would you like to logout of IdentityServer?</p>
</div>
<form asp-page="/Account/Logout/Index">
<input type="hidden" name="logoutId" value="@Model.LogoutId" />
<div class="form-group">
<button class="btn btn-primary">Yes</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,101 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Services;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Logout
{
[SecurityHeaders]
[AllowAnonymous]
public class Index : PageModel
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IEventService _events;
[BindProperty]
public string? LogoutId { get; set; }
public Index(IIdentityServerInteractionService interaction, IEventService events)
{
_interaction = interaction;
_events = events;
}
public async Task<IActionResult> OnGet(string? logoutId)
{
LogoutId = logoutId;
var showLogoutPrompt = LogoutOptions.ShowLogoutPrompt;
if (User.Identity?.IsAuthenticated != true)
{
// if the user is not authenticated, then just show logged out page
showLogoutPrompt = false;
}
else
{
var context = await _interaction.GetLogoutContextAsync(LogoutId);
if (context?.ShowSignoutPrompt == false)
{
// it's safe to automatically sign-out
showLogoutPrompt = false;
}
}
if (showLogoutPrompt == false)
{
// if the request for logout was properly authenticated from IdentityServer, then
// we don't need to show the prompt and can just log the user out directly.
return await OnPost();
}
return Page();
}
public async Task<IActionResult> OnPost()
{
if (User.Identity?.IsAuthenticated == true)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// this can still return null if there is no context needed
LogoutId ??= await _interaction.CreateLogoutContextAsync();
// delete local authentication cookie
await HttpContext.SignOutAsync();
// see if we need to trigger federated logout
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
Telemetry.Metrics.UserLogout(idp);
// if it's a local login we can ignore this workflow
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
{
// we need to see if the provider supports external logout
if (await HttpContext.GetSchemeSupportsSignOutAsync(idp))
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Page("/Account/Logout/Loggedout", new { logoutId = LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);
}
}
}
return RedirectToPage("/Account/Logout/LoggedOut", new { logoutId = LogoutId });
}
}
}

View File

@@ -0,0 +1,30 @@
@page
@model Microser.IdS.Pages.Logout.LoggedOut
<div class="logged-out-page">
<h1>
Logout
<small>You are now logged out</small>
</h1>
@if (Model.View.PostLogoutRedirectUri != null)
{
<div>
Click <a class="PostLogoutRedirectUri" href="@Model.View.PostLogoutRedirectUri">here</a> to return to the
<span>@Model.View.ClientName</span> application.
</div>
}
@if (Model.View.SignOutIframeUrl != null)
{
<iframe width="0" height="0" class="signout" src="@Model.View.SignOutIframeUrl"></iframe>
}
</div>
@section scripts
{
@if (Model.View.AutomaticRedirectAfterSignOut)
{
<script src="~/js/signout-redirect.js"></script>
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Logout
{
[SecurityHeaders]
[AllowAnonymous]
public class LoggedOut : PageModel
{
private readonly IIdentityServerInteractionService _interactionService;
public LoggedOutViewModel View { get; set; } = default!;
public LoggedOut(IIdentityServerInteractionService interactionService)
{
_interactionService = interactionService;
}
public async Task OnGet(string? logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interactionService.GetLogoutContextAsync(logoutId);
View = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = LogoutOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = String.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Logout
{
public class LoggedOutViewModel
{
public string? PostLogoutRedirectUri { get; set; }
public string? ClientName { get; set; }
public string? SignOutIframeUrl { get; set; }
public bool AutomaticRedirectAfterSignOut { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Logout
{
public static class LogoutOptions
{
public static readonly bool ShowLogoutPrompt = true;
public static readonly bool AutomaticRedirectAfterSignOut = false;
}
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.EntityFramework.DbContexts;
using Duende.IdentityServer.EntityFramework.Entities;
using Duende.IdentityServer.EntityFramework.Mappers;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace Microser.IdS.Pages.Admin.ApiScopes
{
public class ApiScopeSummaryModel
{
[Required]
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
}
public class ApiScopeModel : ApiScopeSummaryModel
{
public string? UserClaims { get; set; } = default!;
}
public class ApiScopeRepository
{
private readonly ConfigurationDbContext _context;
public ApiScopeRepository(ConfigurationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<ApiScopeSummaryModel>> GetAllAsync(string? filter = null)
{
var query = _context.ApiScopes
.Include(x => x.UserClaims)
.AsQueryable();
if (!String.IsNullOrWhiteSpace(filter))
{
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
}
var result = query.Select(x => new ApiScopeSummaryModel
{
Name = x.Name,
DisplayName = x.DisplayName
});
return await result.ToArrayAsync();
}
public async Task<ApiScopeModel?> GetByIdAsync(string id)
{
var scope = await _context.ApiScopes
.Include(x => x.UserClaims)
.SingleOrDefaultAsync(x => x.Name == id);
if (scope == null) return null;
return new ApiScopeModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
};
}
public async Task CreateAsync(ApiScopeModel model)
{
ArgumentNullException.ThrowIfNull(model);
var scope = new Duende.IdentityServer.Models.ApiScope()
{
Name = model.Name,
DisplayName = model.DisplayName?.Trim()
};
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
if (claims.Any())
{
scope.UserClaims = claims.ToList();
}
#pragma warning disable CA1849 // Call async methods when in an async method
// CA1849 Suppressed because AddAsync is only needed for value generators that
// need async database access (e.g., HiLoValueGenerator), and we don't use those
// generators
_context.ApiScopes.Add(scope.ToEntity());
#pragma warning restore CA1849 // Call async methods when in an async method
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(ApiScopeModel model)
{
ArgumentNullException.ThrowIfNull(model);
var scope = await _context.ApiScopes
.Include(x => x.UserClaims)
.SingleOrDefaultAsync(x => x.Name == model.Name);
if (scope == null) throw new ArgumentException("Invalid Api Scope");
if (scope.DisplayName != model.DisplayName)
{
scope.DisplayName = model.DisplayName?.Trim();
}
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
var claimsToAdd = claims.Except(currentClaims).ToArray();
var claimsToRemove = currentClaims.Except(claims).ToArray();
if (claimsToRemove.Length != 0)
{
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
}
if (claimsToAdd.Length != 0)
{
scope.UserClaims.AddRange(claimsToAdd.Select(x => new ApiScopeClaim
{
Type = x,
}));
}
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(string id)
{
var scope = await _context.ApiScopes.SingleOrDefaultAsync(x => x.Name == id);
if (scope == null) throw new ArgumentException("Invalid Api Scope");
_context.ApiScopes.Remove(scope);
await _context.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,39 @@
@page
@model Microser.IdS.Pages.Admin.ApiScopes.EditModel
@{
}
<div class="identityscopes-page">
<h2>
Edit Identity Scope: @Model.InputModel.Name
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<input type="hidden" asp-for="@Model.InputModel.Name" />
<div class="form-group">
<label asp-for="@Model.InputModel.DisplayName"></label>
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,59 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.ApiScopes
{
[SecurityHeaders]
[Authorize]
public class EditModel : PageModel
{
private readonly ApiScopeRepository _repository;
public EditModel(ApiScopeRepository repository)
{
_repository = repository;
}
[BindProperty]
public ApiScopeModel InputModel { get; set; } = default!;
[BindProperty]
public string Button { get; set; } = default!;
public async Task<IActionResult> OnGetAsync(string id)
{
var model = await _repository.GetByIdAsync(id);
if (model == null)
{
return RedirectToPage("/Admin/ApiScopes/Index");
}
else
{
InputModel = model;
return Page();
}
}
public async Task<IActionResult> OnPostAsync(string id)
{
if (Button == "delete")
{
await _repository.DeleteAsync(id);
return RedirectToPage("/Admin/ApiScopes/Index");
}
if (ModelState.IsValid)
{
await _repository.UpdateAsync(InputModel);
return RedirectToPage("/Admin/ApiScopes/Edit", new { id });
}
return Page();
}
}
}

View File

@@ -0,0 +1,54 @@
@page
@model Microser.IdS.Pages.Admin.ApiScopes.IndexModel
@{
}
<div class="clients-page">
<div class="row">
<h2>
API Scopes
</h2>
</div>
<div class="row">
<div class="col-md-6">
<form class="row">
<div class="form-group">
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
</div>
<div class="form-group">
<button class="btn btn-primary">Search</button>
</div>
<div class="col">
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
<a asp-page="/Admin/ApiScopes/New" class="btn btn-success">New</a>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Name</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>
@foreach(var scope in Model.Scopes)
{
<tr>
<td><a asp-page="/Admin/ApiScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
<td>@scope.DisplayName</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.ApiScopes
{
[SecurityHeaders]
[Authorize]
public class IndexModel : PageModel
{
private readonly ApiScopeRepository _repository;
public IndexModel(ApiScopeRepository repository)
{
_repository = repository;
}
public IEnumerable<ApiScopeSummaryModel> Scopes { get; private set; } = default!;
public string? Filter { get; set; }
public async Task OnGetAsync(string? filter)
{
Filter = filter;
Scopes = await _repository.GetAllAsync(filter);
}
}
}

View File

@@ -0,0 +1,35 @@
@page
@model Microser.IdS.Pages.Admin.ApiScopes.NewModel
@{
}
<div class="clients-page">
<h2>
New API Scope
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="@Model.InputModel.Name"></label>
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.DisplayName"></label>
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/ApiScopes/Index">Cancel</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.ApiScopes
{
[SecurityHeaders]
[Authorize]
public class NewModel : PageModel
{
private readonly ApiScopeRepository _repository;
public NewModel(ApiScopeRepository repository)
{
_repository = repository;
}
[BindProperty]
public ApiScopeModel InputModel { get; set; } = default!;
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
await _repository.CreateAsync(InputModel);
return RedirectToPage("/Admin/ApiScopes/Edit", new { id = InputModel.Name });
}
return Page();
}
}
}

View File

@@ -0,0 +1,234 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.EntityFramework.DbContexts;
using Duende.IdentityServer.EntityFramework.Entities;
using Duende.IdentityServer.EntityFramework.Mappers;
using Duende.IdentityServer.Models;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace Microser.IdS.Pages.Admin.Clients
{
public class ClientSummaryModel
{
[Required]
public string ClientId { get; set; } = default!;
public string? Name { get; set; }
[Required]
public Flow Flow { get; set; }
}
public class CreateClientModel : ClientSummaryModel
{
public string Secret { get; set; } = default!;
}
public class ClientModel : CreateClientModel, IValidatableObject
{
[Required]
public string AllowedScopes { get; set; } = default!;
public string? RedirectUri { get; set; }
public string? InitiateLoginUri { get; set; }
public string? PostLogoutRedirectUri { get; set; }
public string? FrontChannelLogoutUri { get; set; }
public string? BackChannelLogoutUri { get; set; }
private static readonly string[] memberNames = new[] { "RedirectUri" };
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var errors = new List<ValidationResult>();
if (Flow == Flow.CodeFlowWithPkce)
{
if (RedirectUri == null)
{
errors.Add(new ValidationResult("Redirect URI is required.", memberNames));
}
}
return errors;
}
}
public enum Flow
{
ClientCredentials,
CodeFlowWithPkce
}
public class ClientRepository
{
private readonly ConfigurationDbContext _context;
public ClientRepository(ConfigurationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<ClientSummaryModel>> GetAllAsync(string? filter = null)
{
var grants = new[] { GrantType.AuthorizationCode, GrantType.ClientCredentials };
var query = _context.Clients
.Include(x => x.AllowedGrantTypes)
.Where(x => x.AllowedGrantTypes.Count == 1 && x.AllowedGrantTypes.Any(grant => grants.Contains(grant.GrantType)));
if (!String.IsNullOrWhiteSpace(filter))
{
query = query.Where(x => x.ClientId.Contains(filter) || x.ClientName.Contains(filter));
}
var result = query.Select(x => new ClientSummaryModel
{
ClientId = x.ClientId,
Name = x.ClientName,
Flow = x.AllowedGrantTypes.Select(x => x.GrantType).Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce
});
return await result.ToArrayAsync();
}
public async Task<ClientModel?> GetByIdAsync(string id)
{
var client = await _context.Clients
.Include(x => x.AllowedGrantTypes)
.Include(x => x.AllowedScopes)
.Include(x => x.RedirectUris)
.Include(x => x.PostLogoutRedirectUris)
.Where(x => x.ClientId == id)
.SingleOrDefaultAsync();
if (client == null) return null;
return new ClientModel
{
ClientId = client.ClientId,
Name = client.ClientName,
Flow = client.AllowedGrantTypes.Select(x => x.GrantType)
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce,
AllowedScopes = client.AllowedScopes.Count != 0 ? client.AllowedScopes.Select(x => x.Scope).Aggregate((a, b) => $"{a} {b}") : string.Empty,
RedirectUri = client.RedirectUris.Select(x => x.RedirectUri).SingleOrDefault(),
InitiateLoginUri = client.InitiateLoginUri,
PostLogoutRedirectUri = client.PostLogoutRedirectUris.Select(x => x.PostLogoutRedirectUri).SingleOrDefault(),
FrontChannelLogoutUri = client.FrontChannelLogoutUri,
BackChannelLogoutUri = client.BackChannelLogoutUri,
};
}
public async Task CreateAsync(CreateClientModel model)
{
ArgumentNullException.ThrowIfNull(model);
var client = new Duende.IdentityServer.Models.Client();
client.ClientId = model.ClientId.Trim();
client.ClientName = model.Name?.Trim();
client.ClientSecrets.Add(new Duende.IdentityServer.Models.Secret(model.Secret.Sha256()));
if (model.Flow == Flow.ClientCredentials)
{
client.AllowedGrantTypes = GrantTypes.ClientCredentials;
}
else
{
client.AllowedGrantTypes = GrantTypes.Code;
client.AllowOfflineAccess = true;
}
#pragma warning disable CA1849 // Call async methods when in an async method
// CA1849 Suppressed because AddAsync is only needed for value generators that
// need async database access (e.g., HiLoValueGenerator), and we don't use those
// generators
_context.Clients.Add(client.ToEntity());
#pragma warning restore CA1849 // Call async methods when in an async method
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(ClientModel model)
{
ArgumentNullException.ThrowIfNull(model);
var client = await _context.Clients
.Include(x => x.AllowedGrantTypes)
.Include(x => x.AllowedScopes)
.Include(x => x.RedirectUris)
.Include(x => x.PostLogoutRedirectUris)
.SingleOrDefaultAsync(x => x.ClientId == model.ClientId);
if (client == null) throw new ArgumentException("Invalid Client Id");
if (client.ClientName != model.Name)
{
client.ClientName = model.Name?.Trim();
}
var scopes = model.AllowedScopes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
var currentScopes = (client.AllowedScopes.Select(x => x.Scope) ?? Enumerable.Empty<String>()).ToArray();
var scopesToAdd = scopes.Except(currentScopes).ToArray();
var scopesToRemove = currentScopes.Except(scopes).ToArray();
if (scopesToRemove.Length != 0)
{
client.AllowedScopes.RemoveAll(x => scopesToRemove.Contains(x.Scope));
}
if (scopesToAdd.Length != 0)
{
client.AllowedScopes.AddRange(scopesToAdd.Select(x => new ClientScope
{
Scope = x,
}));
}
var flow = client.AllowedGrantTypes.Select(x => x.GrantType)
.Single() == GrantType.ClientCredentials ? Flow.ClientCredentials : Flow.CodeFlowWithPkce;
if (flow == Flow.CodeFlowWithPkce)
{
if (client.RedirectUris.SingleOrDefault()?.RedirectUri != model.RedirectUri)
{
client.RedirectUris.Clear();
if (model.RedirectUri != null)
{
client.RedirectUris.Add(new ClientRedirectUri { RedirectUri = model.RedirectUri.Trim() });
}
}
if (client.InitiateLoginUri != model.InitiateLoginUri)
{
client.InitiateLoginUri = model.InitiateLoginUri;
}
if (client.PostLogoutRedirectUris.SingleOrDefault()?.PostLogoutRedirectUri != model.PostLogoutRedirectUri)
{
client.PostLogoutRedirectUris.Clear();
if (model.PostLogoutRedirectUri != null)
{
client.PostLogoutRedirectUris.Add(new ClientPostLogoutRedirectUri { PostLogoutRedirectUri = model.PostLogoutRedirectUri.Trim() });
}
}
if (client.FrontChannelLogoutUri != model.FrontChannelLogoutUri)
{
client.FrontChannelLogoutUri = model.FrontChannelLogoutUri?.Trim();
}
if (client.BackChannelLogoutUri != model.BackChannelLogoutUri)
{
client.BackChannelLogoutUri = model.BackChannelLogoutUri?.Trim();
}
}
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(string clientId)
{
var client = await _context.Clients.SingleOrDefaultAsync(x => x.ClientId == clientId);
if (client == null) throw new ArgumentException("Invalid Client Id");
_context.Clients.Remove(client);
await _context.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,65 @@
@page
@model Microser.IdS.Pages.Admin.Clients.EditModel
@{
}
<div class="clients-page">
<h2>
Edit Client Id: @Model.InputModel.ClientId
(<small>@Model.InputModel.Flow</small>)
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<input type="hidden" asp-for="@Model.InputModel.ClientId" />
<input type="hidden" asp-for="@Model.InputModel.Flow" />
<div class="form-group">
<label asp-for="@Model.InputModel.Name"></label>
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.AllowedScopes">Allowed Scopes (space delimited)</label>
<input class="form-control" asp-for="@Model.InputModel.AllowedScopes" />
</div>
@if (Model.InputModel.Flow == Microser.IdS.Pages.Admin.Clients.Flow.CodeFlowWithPkce)
{
<div class="form-group">
<label asp-for="@Model.InputModel.RedirectUri"></label>
<input class="form-control" asp-for="@Model.InputModel.RedirectUri" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.InitiateLoginUri"></label>
<input class="form-control" asp-for="@Model.InputModel.InitiateLoginUri" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.PostLogoutRedirectUri"></label>
<input class="form-control" asp-for="@Model.InputModel.PostLogoutRedirectUri" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.FrontChannelLogoutUri"></label>
<input class="form-control" asp-for="@Model.InputModel.FrontChannelLogoutUri" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.BackChannelLogoutUri"></label>
<input class="form-control" asp-for="@Model.InputModel.BackChannelLogoutUri" />
</div>
}
<div class="form-group">
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.Clients
{
[SecurityHeaders]
[Authorize]
public class EditModel : PageModel
{
private readonly ClientRepository _repository;
public EditModel(ClientRepository repository)
{
_repository = repository;
}
[BindProperty]
public ClientModel InputModel { get; set; } = default!;
[BindProperty]
public string? Button { get; set; }
public async Task<IActionResult> OnGetAsync(string id)
{
var model = await _repository.GetByIdAsync(id);
if (model == null)
{
return RedirectToPage("/Admin/Clients/Index");
}
else
{
InputModel = model;
return Page();
}
}
public async Task<IActionResult> OnPostAsync(string id)
{
if (Button == "delete")
{
await _repository.DeleteAsync(id);
return RedirectToPage("/Admin/Clients/Index");
}
if (ModelState.IsValid)
{
await _repository.UpdateAsync(InputModel);
return RedirectToPage("/Admin/Clients/Edit", new { id });
}
return Page();
}
}
}

View File

@@ -0,0 +1,56 @@
@page
@model Microser.IdS.Pages.Admin.Clients.IndexModel
@{
}
<div class="clients-page">
<div class="row">
<h2>
Clients
</h2>
</div>
<div class="row">
<div class="col-md-6">
<form class="row">
<div class="form-group">
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
</div>
<div class="form-group">
<button class="btn btn-primary">Search</button>
</div>
<div class="col">
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
<a asp-page="/Admin/Clients/New" class="btn btn-success">New</a>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Client ID</th>
<th>Name</th>
<th>Flow</th>
</tr>
</thead>
<tbody>
@foreach(var client in Model.Clients)
{
<tr>
<td><a asp-page="/Admin/Clients/Edit" asp-route-id="@client.ClientId">@client.ClientId</a></td>
<td>@client.Name</td>
<td>@client.Flow</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.Clients
{
[SecurityHeaders]
[Authorize]
public class IndexModel : PageModel
{
private readonly ClientRepository _repository;
public IndexModel(ClientRepository repository)
{
_repository = repository;
}
public IEnumerable<ClientSummaryModel> Clients { get; private set; } = default!;
public string? Filter { get; set; }
public async Task OnGetAsync(string? filter)
{
Filter = filter;
Clients = await _repository.GetAllAsync(filter);
}
}
}

View File

@@ -0,0 +1,56 @@
@page
@model Microser.IdS.Pages.Admin.Clients.NewModel
@{
}
<div class="clients-page">
@if (Model.Created)
{
<div class="row">
<div class="col-md-6">
<h2>Client Id <em>@Model.InputModel.ClientId</em> created</h2>
<p>The client secret is displayed below. Copy it now, as it won't be shown again.</p>
<p class="alert alert-danger">@Model.InputModel.Secret</p>
<p>Click here to <a asp-page="/Admin/Clients/Edit" asp-route-id="@Model.InputModel.ClientId">continue</a>.</p>
</div>
</div>
}
else
{
<h2>
New Client
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="@Model.InputModel.ClientId"></label>
<input class="form-control" asp-for="@Model.InputModel.ClientId" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.Secret"></label>
<input class="form-control" asp-for="@Model.InputModel.Secret" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.Name"></label>
<input class="form-control" asp-for="@Model.InputModel.Name" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.Flow"></label>
<select class="form-control" asp-for="@Model.InputModel.Flow">
<option value="0">Client Credentials</option>
<option value="1">Code Flow (with PKCE)</option>
</select>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/Clients/Index">Cancel</a>
</div>
</form>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,46 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using IdentityModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.Clients
{
[SecurityHeaders]
[Authorize]
public class NewModel : PageModel
{
private readonly ClientRepository _repository;
public NewModel(ClientRepository repository)
{
_repository = repository;
}
[BindProperty]
public CreateClientModel InputModel { get; set; } = default!;
public bool Created { get; set; }
public void OnGet()
{
InputModel = new CreateClientModel
{
Secret = Convert.ToBase64String(CryptoRandom.CreateRandomKey(16))
};
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
await _repository.CreateAsync(InputModel);
Created = true;
}
return Page();
}
}
}

View File

@@ -0,0 +1,39 @@
@page
@model Microser.IdS.Pages.Admin.IdentityScopes.EditModel
@{
}
<div class="identityscopes-page">
<h2>
Edit Identity Scope: @Model.InputModel.Name
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<input type="hidden" asp-for="@Model.InputModel.Name" />
<div class="form-group">
<label asp-for="@Model.InputModel.DisplayName"></label>
<input class="form-control" asp-for="@Model.InputModel.DisplayName" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit" name="Button" value="save">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
<button class="btn btn-danger" type="submit" name="Button" value="delete">Delete</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.IdentityScopes
{
[SecurityHeaders]
[Authorize]
public class EditModel : PageModel
{
private readonly IdentityScopeRepository _repository;
public EditModel(IdentityScopeRepository repository)
{
_repository = repository;
}
[BindProperty]
public IdentityScopeModel InputModel { get; set; } = default!;
[BindProperty]
public string? Button { get; set; }
public async Task<IActionResult> OnGetAsync(string id)
{
var model = await _repository.GetByIdAsync(id);
if (model == null)
{
return RedirectToPage("/Admin/IdentityScopes/Index");
}
else
{
InputModel = model;
return Page();
}
}
public async Task<IActionResult> OnPostAsync(string id)
{
if (Button == "delete")
{
await _repository.DeleteAsync(id);
return RedirectToPage("/Admin/IdentityScopes/Index");
}
if (ModelState.IsValid)
{
await _repository.UpdateAsync(InputModel);
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id });
}
return Page();
}
}
}

View File

@@ -0,0 +1,138 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.EntityFramework.DbContexts;
using Duende.IdentityServer.EntityFramework.Entities;
using Duende.IdentityServer.EntityFramework.Mappers;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace Microser.IdS.Pages.Admin.IdentityScopes
{
public class IdentityScopeSummaryModel
{
[Required]
public string Name { get; set; } = default!;
public string? DisplayName { get; set; }
}
public class IdentityScopeModel : IdentityScopeSummaryModel
{
public string? UserClaims { get; set; }
}
public class IdentityScopeRepository
{
private readonly ConfigurationDbContext _context;
public IdentityScopeRepository(ConfigurationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<IdentityScopeSummaryModel>> GetAllAsync(string? filter = null)
{
var query = _context.IdentityResources
.Include(x => x.UserClaims)
.AsQueryable();
if (!String.IsNullOrWhiteSpace(filter))
{
query = query.Where(x => x.Name.Contains(filter) || x.DisplayName.Contains(filter));
}
var result = query.Select(x => new IdentityScopeSummaryModel
{
Name = x.Name,
DisplayName = x.DisplayName
});
return await result.ToArrayAsync();
}
public async Task<IdentityScopeModel?> GetByIdAsync(string id)
{
var scope = await _context.IdentityResources
.Include(x => x.UserClaims)
.SingleOrDefaultAsync(x => x.Name == id);
if (scope == null) return null;
return new IdentityScopeModel
{
Name = scope.Name,
DisplayName = scope.DisplayName,
UserClaims = scope.UserClaims.Count != 0 ? scope.UserClaims.Select(x => x.Type).Aggregate((a, b) => $"{a} {b}") : null,
};
}
public async Task CreateAsync(IdentityScopeModel model)
{
ArgumentNullException.ThrowIfNull(model);
var scope = new Duende.IdentityServer.Models.IdentityResource()
{
Name = model.Name,
DisplayName = model.DisplayName?.Trim()
};
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
if (claims.Any())
{
scope.UserClaims = claims.ToList();
}
#pragma warning disable CA1849 // Call async methods when in an async method
// CA1849 Suppressed because AddAsync is only needed for value generators that
// need async database access (e.g., HiLoValueGenerator), and we don't use those
// generators
_context.IdentityResources.Add(scope.ToEntity());
#pragma warning restore CA1849
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(IdentityScopeModel model)
{
ArgumentNullException.ThrowIfNull(model);
var scope = await _context.IdentityResources
.Include(x => x.UserClaims)
.SingleOrDefaultAsync(x => x.Name == model.Name);
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
if (scope.DisplayName != model.DisplayName)
{
scope.DisplayName = model.DisplayName?.Trim();
}
var claims = model.UserClaims?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray() ?? Enumerable.Empty<string>();
var currentClaims = (scope.UserClaims.Select(x => x.Type) ?? Enumerable.Empty<String>()).ToArray();
var claimsToAdd = claims.Except(currentClaims).ToArray();
var claimsToRemove = currentClaims.Except(claims).ToArray();
if (claimsToRemove.Length != 0)
{
scope.UserClaims.RemoveAll(x => claimsToRemove.Contains(x.Type));
}
if (claimsToAdd.Length != 0)
{
scope.UserClaims.AddRange(claimsToAdd.Select(x => new IdentityResourceClaim
{
Type = x,
}));
}
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(string id)
{
var scope = await _context.IdentityResources.SingleOrDefaultAsync(x => x.Name == id);
if (scope == null) throw new ArgumentException("Invalid Identity Scope");
_context.IdentityResources.Remove(scope);
await _context.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,54 @@
@page
@model Microser.IdS.Pages.Admin.IdentityScopes.IndexModel
@{
}
<div class="clients-page">
<div class="row">
<h2>
Identity Scopes
</h2>
</div>
<div class="row">
<div class="col-md-6">
<form class="row">
<div class="form-group">
<input class="form-control" placeholder="Filter" id="filter" name="filter" type="text" value="@Model.Filter" autofocus />
</div>
<div class="form-group">
<button class="btn btn-primary">Search</button>
</div>
<div class="col">
<a asp-page="/Admin/Index" class="btn btn-secondary">Cancel</a>
<a asp-page="/Admin/IdentityScopes/New" class="btn btn-success">New</a>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Name</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>
@foreach(var scope in Model.Scopes)
{
<tr>
<td><a asp-page="/Admin/IdentityScopes/Edit" asp-route-id="@scope.Name">@scope.Name</a></td>
<td>@scope.DisplayName</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.IdentityScopes
{
[SecurityHeaders]
[Authorize]
public class IndexModel : PageModel
{
private readonly IdentityScopeRepository _repository;
public IndexModel(IdentityScopeRepository repository)
{
_repository = repository;
}
public IEnumerable<IdentityScopeSummaryModel> Scopes { get; private set; } = default!;
public string? Filter { get; set; }
public async Task OnGetAsync(string? filter)
{
Filter = filter;
Scopes = await _repository.GetAllAsync(filter);
}
}
}

View File

@@ -0,0 +1,35 @@
@page
@model Microser.IdS.Pages.Admin.IdentityScopes.NewModel
@{
}
<div class="clients-page">
<h2>
New Identity Scope
</h2>
<partial name="_ValidationSummary" />
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="@Model.InputModel.Name"></label>
<input class="form-control" asp-for="@Model.InputModel.Name" autofocus />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.DisplayName"></label>
<input class="form-control" asp-for="@Model.InputModel.DisplayName" />
</div>
<div class="form-group">
<label asp-for="@Model.InputModel.UserClaims">User Claims (space delimited)</label>
<input class="form-control" asp-for="@Model.InputModel.UserClaims" />
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Save Changes</button>
<a class="btn btn-secondary" asp-page="/Admin/IdentityScopes/Index">Cancel</a>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin.IdentityScopes
{
[SecurityHeaders]
[Authorize]
public class NewModel : PageModel
{
private readonly IdentityScopeRepository _repository;
public NewModel(IdentityScopeRepository repository)
{
_repository = repository;
}
[BindProperty]
public IdentityScopeModel InputModel { get; set; } = default!;
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
await _repository.CreateAsync(InputModel);
return RedirectToPage("/Admin/IdentityScopes/Edit", new { id = InputModel.Name });
}
return Page();
}
}
}

View File

@@ -0,0 +1,23 @@
@page
@model Microser.IdS.Pages.Admin.IndexModel
@{
}
<div class="admin-page">
<h2>
Duende IdentityServer Admin
</h2>
<div class="row">
<p>There are simple administrative pages.</p>
</div>
<div class="row">
<div class="list-group">
<a class="list-group-item list-group-item-action" asp-page="/Admin/Clients/Index">Clients</a>
<a class="list-group-item list-group-item-action" asp-page="/Admin/IdentityScopes/Index">Identity Scopes</a>
<a class="list-group-item list-group-item-action" asp-page="/Admin/ApiScopes/Index">API Scopes</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Admin
{
[SecurityHeaders]
[Authorize]
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@@ -0,0 +1,48 @@
@page
@model Microser.IdS.Pages.Ciba.AllModel
@{
}
<div class="ciba-page">
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h2>Pending Backchannel Login Requests</h2>
</div>
<div class="card-body">
@if (Model.Logins.Any())
{
<table class="table table-bordered table-striped table-sm">
<thead>
<tr>
<th>Id</th>
<th>Client Id</th>
<th>Binding Message</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var login in Model.Logins)
{
<tr>
<td>@login.InternalId</td>
<td>@login.Client.ClientId</td>
<td>@login.BindingMessage</td>
<td>
<a asp-page="Consent" asp-route-id="@login.InternalId" class="btn btn-primary">Process</a>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<div>No Pending Login Requests</div>
}
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Ciba
{
[SecurityHeaders]
[Authorize]
public class AllModel : PageModel
{
public IEnumerable<BackchannelUserLoginRequest> Logins { get; set; } = default!;
private readonly IBackchannelAuthenticationInteractionService _backchannelAuthenticationInteraction;
public AllModel(IBackchannelAuthenticationInteractionService backchannelAuthenticationInteractionService)
{
_backchannelAuthenticationInteraction = backchannelAuthenticationInteractionService;
}
public async Task OnGet()
{
Logins = await _backchannelAuthenticationInteraction.GetPendingLoginRequestsForCurrentUserAsync();
}
}
}

View File

@@ -0,0 +1,98 @@
@page
@model Microser.IdS.Pages.Ciba.Consent
@{
}
<div class="ciba-consent">
<div class="lead">
@if (Model.View.ClientLogoUrl != null)
{
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
}
<h1>
@Model.View.ClientName
<small class="text-muted">is requesting your permission</small>
</h1>
<h3>Verify that this identifier matches what the client is displaying: <em class="text-primary">@Model.View.BindingMessage</em></h3>
<p>Uncheck the permissions you do not wish to grant.</p>
</div>
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
</div>
</div>
<form asp-page="/Ciba/Consent">
<input type="hidden" asp-for="Input.Id" />
<div class="row">
<div class="col-sm-8">
@if (Model.View.IdentityScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-user"></span>
Personal Information
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.View.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
</div>
}
@if (Model.View.ApiScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-tasks"></span>
Application Access
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.View.ApiScopes)
{
<partial name="_ScopeListItem" model="scope" />
}
</ul>
</div>
</div>
}
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-pencil"></span>
Description
</div>
<div class="card-body">
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
</div>
<div class="col-sm-4 col-lg-auto">
@if (Model.View.ClientUrl != null)
{
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
<span class="glyphicon glyphicon-info-sign"></span>
<strong>@Model.View.ClientName</strong>
</a>
}
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,229 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microser.IdS.Pages.Ciba
{
[Authorize]
[SecurityHeaders]
public class Consent : PageModel
{
private readonly IBackchannelAuthenticationInteractionService _interaction;
private readonly IEventService _events;
private readonly ILogger<Consent> _logger;
public Consent(
IBackchannelAuthenticationInteractionService interaction,
IEventService events,
ILogger<Consent> logger)
{
_interaction = interaction;
_events = events;
_logger = logger;
}
public ViewModel View { get; set; } = default!;
[BindProperty]
public InputModel Input { get; set; } = default!;
public async Task<IActionResult> OnGet(string? id)
{
if (!await SetViewModelAsync(id))
{
return RedirectToPage("/Home/Error/Index");
}
Input = new InputModel
{
Id = id
};
return Page();
}
public async Task<IActionResult> OnPost()
{
// validate return url is still valid
var request = await _interaction.GetLoginRequestByInternalIdAsync(Input.Id ?? throw new ArgumentNullException(nameof(Input.Id)));
if (request == null || request.Subject.GetSubjectId() != User.GetSubjectId())
{
_logger.InvalidId(Input.Id);
return RedirectToPage("/Home/Error/Index");
}
CompleteBackchannelLoginRequest? result = null;
// user clicked 'no' - send back the standard 'access_denied' response
if (Input.Button == "no")
{
result = new CompleteBackchannelLoginRequest(Input.Id);
// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
{
// if the user consented to some scope, build the response model
if (Input.ScopesConsented.Any())
{
var scopes = Input.ScopesConsented;
if (ConsentOptions.EnableOfflineAccess == false)
{
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
}
result = new CompleteBackchannelLoginRequest(Input.Id)
{
ScopesValuesConsented = scopes.ToArray(),
Description = Input.Description
};
// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, result.ScopesValuesConsented, false);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
}
}
else
{
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
}
if (result != null)
{
// communicate outcome of consent back to identityserver
await _interaction.CompleteLoginRequestAsync(result);
return RedirectToPage("/Ciba/All");
}
// we need to redisplay the consent UI
if (!await SetViewModelAsync(Input.Id))
{
return RedirectToPage("/Home/Error/Index");
}
return Page();
}
private async Task<bool> SetViewModelAsync(string? id)
{
ArgumentNullException.ThrowIfNull(id);
var request = await _interaction.GetLoginRequestByInternalIdAsync(id);
if (request != null && request.Subject.GetSubjectId() == User.GetSubjectId())
{
View = CreateConsentViewModel(request);
return true;
}
else
{
_logger.NoMatchingBackchannelLoginRequest(id);
return false;
}
}
private ViewModel CreateConsentViewModel(BackchannelUserLoginRequest request)
{
var vm = new ViewModel
{
ClientName = request.Client.ClientName ?? request.Client.ClientId,
ClientUrl = request.Client.ClientUri,
ClientLogoUrl = request.Client.LogoUri,
BindingMessage = request.BindingMessage
};
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources
.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name)))
.ToArray();
var resourceIndicators = request.RequestedResourceIndicators ?? Enumerable.Empty<string>();
var apiResources = request.ValidatedResources.Resources.ApiResources.Where(x => resourceIndicators.Contains(x.Name));
var apiScopes = new List<ScopeViewModel>();
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
{
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
if (apiScope != null)
{
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
scopeVm.Resources = apiResources.Where(x => x.Scopes.Contains(parsedScope.ParsedName))
.Select(x => new ResourceViewModel
{
Name = x.Name,
DisplayName = x.DisplayName ?? x.Name,
}).ToArray();
apiScopes.Add(scopeVm);
}
}
if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
{
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
}
vm.ApiScopes = apiScopes;
return vm;
}
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ScopeViewModel
{
Name = identity.Name,
Value = identity.Name,
DisplayName = identity.DisplayName ?? identity.Name,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
{
var displayName = apiScope.DisplayName ?? apiScope.Name;
if (!String.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))
{
displayName += ":" + parsedScopeValue.ParsedParameter;
}
return new ScopeViewModel
{
Name = parsedScopeValue.ParsedName,
Value = parsedScopeValue.RawValue,
DisplayName = displayName,
Description = apiScope.Description,
Emphasize = apiScope.Emphasize,
Required = apiScope.Required,
Checked = check || apiScope.Required
};
}
private static ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ScopeViewModel
{
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = ConsentOptions.OfflineAccessDisplayName,
Description = ConsentOptions.OfflineAccessDescription,
Emphasize = true,
Checked = check
};
}
}
}

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