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,16 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Device
{
public static class DeviceOptions
{
public static readonly bool EnableOfflineAccess = true;
public static readonly string OfflineAccessDisplayName = "Offline Access";
public static readonly string OfflineAccessDescription = "Access to your applications and resources, even when you are offline";
public static readonly string InvalidUserCode = "Invalid user code";
public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission";
public static readonly string InvalidSelectionErrorMessage = "Invalid selection";
}
}

View File

@@ -0,0 +1,141 @@
@page
@model Microser.IdS.Pages.Device.Index
@{
}
@if (Model.Input.UserCode == null)
{
@*We need to collect the user code*@
<div class="page-device-code">
<div class="lead">
<h1>User Code</h1>
<p>Please enter the code displayed on your device.</p>
</div>
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
</div>
</div>
<div class="row">
<div class="col-sm-6">
<form asp-page="/Device/Index" method="get">
<div class="form-group">
<label for="userCode">User Code:</label>
<input class="form-control" for="userCode" name="userCode" autofocus />
</div>
<button class="btn btn-primary" name="button">Submit</button>
</form>
</div>
</div>
</div>
}
else
{
@*collect consent for the user code provided*@
<div class="page-device-confirmation">
<div class="lead">
@if (Model.View.ClientLogoUrl != null)
{
<div class="client-logo"><img src="@Model.View.ClientLogoUrl"></div>
}
<h1>
@Model.View.ClientName
<small class="text-muted">is requesting your permission</small>
</h1>
<p>Please confirm that the authorization request matches the code: <strong>@Model.Input.UserCode</strong>.</p>
<p>Uncheck the permissions you do not wish to grant.</p>
</div>
<div class="row">
<div class="col-sm-8">
<partial name="_ValidationSummary" />
</div>
</div>
<form asp-page="/Device/Index">
<input asp-for="Input.UserCode" type="hidden" />
<div class="row">
<div class="col-sm-8">
@if (Model.View.IdentityScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-user"></span>
Personal Information
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.View.IdentityScopes)
{
<partial name="_ScopeListItem" model="@scope" />
}
</ul>
</div>
</div>
}
@if (Model.View.ApiScopes.Any())
{
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-tasks"></span>
Application Access
</div>
<ul class="list-group list-group-flush">
@foreach (var scope in Model.View.ApiScopes)
{
<partial name="_ScopeListItem" model="scope" />
}
</ul>
</div>
</div>
}
<div class="form-group">
<div class="card">
<div class="card-header">
<span class="glyphicon glyphicon-pencil"></span>
Description
</div>
<div class="card-body">
<input class="form-control" placeholder="Description or name of device" asp-for="Input.Description" autofocus>
</div>
</div>
</div>
@if (Model.View.AllowRememberConsent)
{
<div class="form-group">
<div class="form-check">
<input class="form-check-input" asp-for="Input.RememberConsent">
<label class="form-check-label" asp-for="Input.RememberConsent">
<strong>Remember My Decision</strong>
</label>
</div>
</div>
}
</div>
</div>
<div class="row">
<div class="col-sm-4">
<button name="Input.button" value="yes" class="btn btn-primary" autofocus>Yes, Allow</button>
<button name="Input.button" value="no" class="btn btn-secondary">No, Do Not Allow</button>
</div>
<div class="col-sm-4 col-lg-auto">
@if (Model.View.ClientUrl != null)
{
<a class="btn btn-outline-info" href="@Model.View.ClientUrl">
<span class="glyphicon glyphicon-info-sign"></span>
<strong>@Model.View.ClientName</strong>
</a>
}
</div>
</div>
</form>
</div>
}

View File

@@ -0,0 +1,221 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Validation;
using Microser.IdS.Pages.Consent;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace Microser.IdS.Pages.Device
{
[SecurityHeaders]
[Authorize]
public class Index : PageModel
{
private readonly IDeviceFlowInteractionService _interaction;
private readonly IEventService _events;
private readonly IOptions<IdentityServerOptions> _options;
private readonly ILogger<Index> _logger;
public Index(
IDeviceFlowInteractionService interaction,
IEventService eventService,
IOptions<IdentityServerOptions> options,
ILogger<Index> logger)
{
_interaction = interaction;
_events = eventService;
_options = options;
_logger = logger;
}
public ViewModel View { get; set; } = default!;
[BindProperty]
public InputModel Input { get; set; } = default!;
public async Task<IActionResult> OnGet(string? userCode)
{
if (String.IsNullOrWhiteSpace(userCode))
{
return Page();
}
if (!await SetViewModelAsync(userCode))
{
ModelState.AddModelError("", DeviceOptions.InvalidUserCode);
return Page();
}
Input = new InputModel
{
UserCode = userCode,
};
return Page();
}
public async Task<IActionResult> OnPost()
{
var request = await _interaction.GetAuthorizationContextAsync(Input.UserCode ?? throw new ArgumentNullException(nameof(Input.UserCode)));
if (request == null) return RedirectToPage("/Home/Error/Index");
ConsentResponse? grantedConsent = null;
// user clicked 'no' - send back the standard 'access_denied' response
if (Input.Button == "no")
{
grantedConsent = new ConsentResponse
{
Error = AuthorizationError.AccessDenied
};
// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
{
// if the user consented to some scope, build the response model
if (Input.ScopesConsented.Any())
{
var scopes = Input.ScopesConsented;
if (ConsentOptions.EnableOfflineAccess == false)
{
scopes = scopes.Where(x => x != Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess);
}
grantedConsent = new ConsentResponse
{
RememberConsent = Input.RememberConsent,
ScopesValuesConsented = scopes.ToArray(),
Description = Input.Description
};
// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
ModelState.AddModelError("", ConsentOptions.MustChooseOneErrorMessage);
}
}
else
{
ModelState.AddModelError("", ConsentOptions.InvalidSelectionErrorMessage);
}
if (grantedConsent != null)
{
// communicate outcome of consent back to identityserver
await _interaction.HandleRequestAsync(Input.UserCode, grantedConsent);
// indicate that's it ok to redirect back to authorization endpoint
return RedirectToPage("/Device/Success");
}
// we need to redisplay the consent UI
if (!await SetViewModelAsync(Input.UserCode))
{
return RedirectToPage("/Home/Error/Index");
}
return Page();
}
private async Task<bool> SetViewModelAsync(string userCode)
{
var request = await _interaction.GetAuthorizationContextAsync(userCode);
if (request != null)
{
View = CreateConsentViewModel(request);
return true;
}
else
{
View = new ViewModel();
return false;
}
}
private ViewModel CreateConsentViewModel(DeviceFlowAuthorizationRequest request)
{
var vm = new ViewModel
{
ClientName = request.Client.ClientName ?? request.Client.ClientId,
ClientUrl = request.Client.ClientUri,
ClientLogoUrl = request.Client.LogoUri,
AllowRememberConsent = request.Client.AllowRememberConsent
};
vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, Input == null || Input.ScopesConsented.Contains(x.Name))).ToArray();
var apiScopes = new List<ScopeViewModel>();
foreach (var parsedScope in request.ValidatedResources.ParsedScopes)
{
var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);
if (apiScope != null)
{
var scopeVm = CreateScopeViewModel(parsedScope, apiScope, Input == null || Input.ScopesConsented.Contains(parsedScope.RawValue));
apiScopes.Add(scopeVm);
}
}
if (DeviceOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)
{
apiScopes.Add(GetOfflineAccessScope(Input == null || Input.ScopesConsented.Contains(Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess)));
}
vm.ApiScopes = apiScopes;
return vm;
}
private static ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)
{
return new ScopeViewModel
{
Value = identity.Name,
DisplayName = identity.DisplayName ?? identity.Name,
Description = identity.Description,
Emphasize = identity.Emphasize,
Required = identity.Required,
Checked = check || identity.Required
};
}
private static ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)
{
return new ScopeViewModel
{
Value = parsedScopeValue.RawValue,
// todo: use the parsed scope value in the display?
DisplayName = apiScope.DisplayName ?? apiScope.Name,
Description = apiScope.Description,
Emphasize = apiScope.Emphasize,
Required = apiScope.Required,
Checked = check || apiScope.Required
};
}
private static ScopeViewModel GetOfflineAccessScope(bool check)
{
return new ScopeViewModel
{
Value = Duende.IdentityServer.IdentityServerConstants.StandardScopes.OfflineAccess,
DisplayName = DeviceOptions.OfflineAccessDisplayName,
Description = DeviceOptions.OfflineAccessDescription,
Emphasize = true,
Checked = check
};
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Device
{
public class InputModel
{
public string? Button { get; set; }
public IEnumerable<string> ScopesConsented { get; set; } = new List<string>();
public bool RememberConsent { get; set; } = true;
public string? ReturnUrl { get; set; }
public string? Description { get; set; }
public string? UserCode { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
@page
@model Microser.IdS.Pages.Device.SuccessModel
@{
}
<div class="page-device-success">
<div class="lead">
<h1>Success</h1>
<p>You have successfully authorized the device</p>
</div>
</div>

View File

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

View File

@@ -0,0 +1,26 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Microser.IdS.Pages.Device
{
public class ViewModel
{
public string? ClientName { get; set; }
public string? ClientUrl { get; set; }
public string? ClientLogoUrl { get; set; }
public bool AllowRememberConsent { get; set; }
public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
public IEnumerable<ScopeViewModel> ApiScopes { get; set; } = Enumerable.Empty<ScopeViewModel>();
}
public class ScopeViewModel
{
public string? Value { get; set; }
public string? DisplayName { get; set; }
public string? Description { get; set; }
public bool Emphasize { get; set; }
public bool Required { get; set; }
public bool Checked { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
@using Microser.IdS.Pages.Device
@model ScopeViewModel
<li class="list-group-item">
<label>
<input class="consent-scopecheck"
type="checkbox"
name="Input.ScopesConsented"
id="scopes_@Model.Value"
value="@Model.Value"
checked="@Model.Checked"
disabled="@Model.Required" />
@if (Model.Required)
{
<input type="hidden"
name="Input.ScopesConsented"
value="@Model.Value" />
}
<strong>@Model.DisplayName</strong>
@if (Model.Emphasize)
{
<span class="glyphicon glyphicon-exclamation-sign"></span>
}
</label>
@if (Model.Required)
{
<span><em>(required)</em></span>
}
@if (Model.Description != null)
{
<div class="consent-description">
<label for="scopes_@Model.Value">@Model.Description</label>
</div>
}
</li>