added Microser
This commit is contained in:
16
src/Microser/Microser.IdS/Pages/Device/DeviceOptions.cs
Normal file
16
src/Microser/Microser.IdS/Pages/Device/DeviceOptions.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
141
src/Microser/Microser.IdS/Pages/Device/Index.cshtml
Normal file
141
src/Microser/Microser.IdS/Pages/Device/Index.cshtml
Normal 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>
|
||||
}
|
||||
221
src/Microser/Microser.IdS/Pages/Device/Index.cshtml.cs
Normal file
221
src/Microser/Microser.IdS/Pages/Device/Index.cshtml.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Microser/Microser.IdS/Pages/Device/InputModel.cs
Normal file
15
src/Microser/Microser.IdS/Pages/Device/InputModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
12
src/Microser/Microser.IdS/Pages/Device/Success.cshtml
Normal file
12
src/Microser/Microser.IdS/Pages/Device/Success.cshtml
Normal 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>
|
||||
17
src/Microser/Microser.IdS/Pages/Device/Success.cshtml.cs
Normal file
17
src/Microser/Microser.IdS/Pages/Device/Success.cshtml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microser.IdS.Pages.Device
|
||||
{
|
||||
[SecurityHeaders]
|
||||
[Authorize]
|
||||
public class SuccessModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/Microser/Microser.IdS/Pages/Device/ViewModel.cs
Normal file
26
src/Microser/Microser.IdS/Pages/Device/ViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
35
src/Microser/Microser.IdS/Pages/Device/_ScopeListItem.cshtml
Normal file
35
src/Microser/Microser.IdS/Pages/Device/_ScopeListItem.cshtml
Normal 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>
|
||||
Reference in New Issue
Block a user