Skip to content

Commit

Permalink
Add One Login header
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad committed Apr 4, 2024
1 parent e848e83 commit d113da9
Show file tree
Hide file tree
Showing 28 changed files with 965 additions and 180 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public string Trn(JourneyInstanceId journeyInstanceId) =>
public string NotFound(JourneyInstanceId journeyInstanceId) =>
GetRequiredPathByPage("/NotFound", journeyInstanceId: journeyInstanceId);

public string SignOut(JourneyInstanceId journeyInstanceId) =>
GetRequiredPathByPage("/SignOut", journeyInstanceId: journeyInstanceId);

protected virtual string GetRequiredPathByPage(string page, string? handler = null, object? routeValues = null, JourneyInstanceId? journeyInstanceId = null)
{
var url = GetRequiredPathByPage(page, handler, routeValues);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ public async Task<IActionResult> Authorize()
{
var parameters = Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList();

var serviceUrl = new Uri(request.RedirectUri!).GetLeftPart(UriPartial.Authority);

var authenticationProperties = new AuthenticationProperties()
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
};
authenticationProperties.Items.Add("OneLoginAuthenticationScheme", client.OneLoginAuthenticationSchemeName);
authenticationProperties.Items.Add(MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.OneLoginAuthenticationScheme, client.OneLoginAuthenticationSchemeName);
authenticationProperties.Items.Add(MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceName, client.Name);
authenticationProperties.Items.Add(MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceUrl, serviceUrl);

return Challenge(authenticationProperties, childAuthenticationScheme);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.FormFlow;
using TeachingRecordSystem.FormFlow.State;

namespace TeachingRecordSystem.AuthorizeAccess.Infrastructure.Filters;

public class AssignViewDataFromFormFlowJourneyResultFilter(IUserInstanceStateProvider stateProvider, AuthorizeAccessLinkGenerator linkGenerator) : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
var journeyInstance = await stateProvider.GetSignInJourneyInstanceAsync(context.HttpContext);

if (journeyInstance is not null && context.Result is PageResult pageResult)
{
pageResult.ViewData.Add("ServiceName", journeyInstance.State.ServiceName);
pageResult.ViewData.Add("ServiceUrl", journeyInstance.State.ServiceUrl);
pageResult.ViewData.Add("SignOutLink", linkGenerator.SignOut(journeyInstance.InstanceId));
}

await next();
}
}

public class AssignViewDataFromFormFlowJourneyResultFilterFactory : IFilterFactory
{
public bool IsReusable => false;

public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) =>
ActivatorUtilities.CreateInstance<AssignViewDataFromFormFlowJourneyResultFilter>(serviceProvider);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,19 @@ public async Task ChallengeAsync(AuthenticationProperties? properties)
EnsureInitialized();

if (properties is null ||
!properties.Items.TryGetValue("OneLoginAuthenticationScheme", out var oneLoginAuthenticationScheme) ||
string.IsNullOrEmpty(oneLoginAuthenticationScheme))
!properties.Items.TryGetValue(AuthenticationPropertiesItemKeys.OneLoginAuthenticationScheme, out var oneLoginAuthenticationScheme) ||
oneLoginAuthenticationScheme is null ||
!properties.Items.TryGetValue(AuthenticationPropertiesItemKeys.ServiceName, out var serviceName) ||
serviceName is null ||
!properties.Items.TryGetValue(AuthenticationPropertiesItemKeys.ServiceUrl, out var serviceUrl) ||
serviceUrl is null)
{
throw new InvalidOperationException($"'OneLoginAuthenticationScheme' must be passed in {nameof(properties)}.{nameof(properties.Items)}.");
throw new InvalidOperationException($"{nameof(AuthenticationProperties)} is missing one or more items.");
}

var journeyInstance = await helper.UserInstanceStateProvider.GetOrCreateSignInJourneyInstanceAsync(
_context,
createState: () => new SignInJourneyState(properties.RedirectUri ?? "/", oneLoginAuthenticationScheme, properties),
createState: () => new SignInJourneyState(properties.RedirectUri ?? "/", serviceName, serviceUrl, oneLoginAuthenticationScheme),
updateState: state => state.Reset());

var result = helper.SignInWithOneLogin(journeyInstance);
Expand All @@ -70,4 +74,11 @@ private void EnsureInitialized()
throw new InvalidOperationException("Not initialized.");
}
}

public static class AuthenticationPropertiesItemKeys
{
public const string OneLoginAuthenticationScheme = "OneLoginAuthenticationScheme";
public const string ServiceName = "ServiceName";
public const string ServiceUrl = "ServiceUrl";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,6 @@ void IConfigureNamedOptions<OneLoginOptions>.Configure(string? name, OneLoginOpt

options.SignInScheme = AuthenticationSchemes.FormFlowJourney;

options.Events.OnRedirectToIdentityProvider = context =>
{
// A large RedirectUri here can make the state parameter so large that One Login rejects the request.
// We have the RedirectUri stashed away on the FormFlow journey any way so we can clear it out.
context.Properties.RedirectUri = null;

return Task.CompletedTask;
};

options.Events.OnRedirectToIdentityProviderForSignOut = context =>
{
// The standard sign out process will call Authenticate() on SignInScheme then try to extract the id_token from the Principal.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@page "/connect/logout"
@using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.Extensions.Primitives
@using TeachingRecordSystem.AuthorizeAccess.Pages.Oidc
@addTagHelper *, Joonasw.AspNetCore.SecurityHeaders
@model SignOutModel
@{
ViewBag.Title = $"Sign out of {Model.ServiceName}";
ViewBag.ServiceName = Model.ServiceName;
ViewBag.ServiceUrl = Model.ServiceUrl;
ViewBag.SignOutLink = Request.GetEncodedUrl();
}

<form asp-page="SignOut" method="post">
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>

@foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)HttpContext.Request.Form : HttpContext.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}

<govuk-button type="submit">Sign out</govuk-button>
</div>
</div>
</form>

<script asp-add-nonce="true">document.forms[0].submit();</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Security.Claims;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using TeachingRecordSystem.Core.DataStore.Postgres;
using TeachingRecordSystem.Core.DataStore.Postgres.Models;

namespace TeachingRecordSystem.AuthorizeAccess.Pages.Oidc;

public class SignOutModel(TrsDbContext dbContext) : PageModel
{
private OpenIddictRequest? _request;
private AuthenticateResult? _authenticateResult;
private ApplicationUser? _client;

public string ServiceName => _client!.Name;
public string ServiceUrl => new Uri(_request!.PostLogoutRedirectUri!).GetLeftPart(UriPartial.Authority);

public void OnGet()
{
}

public async Task<IActionResult> OnPost()
{
// We need to sign out with One Login and then complete the OIDC sign out request.
// We do it by calling SignOutAsync with OpenIddict first, capturing the Location header from its redirect
// then redirecting to OneLogin with that URL as the RedirectUri.

await HttpContext.SignOutAsync(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties()
{
RedirectUri = "/"
});

var authenticationProperties = new AuthenticationProperties()
{
RedirectUri = HttpContext.Response.Headers.Location
};
var oneLoginIdToken = _authenticateResult!.Principal!.FindFirstValue(ClaimTypes.OneLoginIdToken)!;
authenticationProperties.SetParameter(OpenIdConnectParameterNames.IdToken, oneLoginIdToken);

return SignOut(authenticationProperties, _client!.OneLoginAuthenticationSchemeName!);
}

public async override Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
{
// Although the spec allows for logout requests without an id_token_hint, we require one so we can
// a) extract the One Login ID token and;
// b) know which authentication scheme to sign out with.

_request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

if (_request.IdTokenHint is null)
{
context.Result = BadRequest();
return;
}

_authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

string clientId = _authenticateResult.Principal!.GetAudiences().Single();
_client = await dbContext.ApplicationUsers.SingleAsync(u => u.ClientId == clientId);

await base.OnPageHandlerExecutionAsync(context, next);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,82 @@
@{
Layout = "../Shared/_Layout";

ViewBag.ServiceName = "OIDC Sample";
Layout = "_GovUkPageTemplate";
}

@section Styles {
@section Head {
<meta name="robots" content="noindex">
<link rel="stylesheet" asp-href-include="~/Styles/*.css">
@RenderSection("Styles", required: false)
@RenderSection("Scripts", required: false)
}

@section Scripts {
@RenderSection("Scripts", required: false)
@section Header {
<header class="govuk-header" role="banner" data-module="govuk-header">
<div class="govuk-header__container govuk-width-container">
<div class="govuk-header__logo">
<a href="#" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<!--[if gt IE 8]><!-->
<svg aria-hidden="true"
focusable="false"
class="govuk-header__logotype-crown"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 30"
height="30"
width="32">
<path fill="currentColor" fill-rule="evenodd"
d="M22.6 10.4c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m-5.9 6.7c-.9.4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m10.8-3.7c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s0 2-1 2.4m3.3 4.8c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4M17 4.7l2.3 1.2V2.5l-2.3.7-.2-.2.9-3h-3.4l.9 3-.2.2c-.1.1-2.3-.7-2.3-.7v3.4L15 4.7c.1.1.1.2.2.2l-1.3 4c-.1.2-.1.4-.1.6 0 1.1.8 2 1.9 2.2h.7c1-.2 1.9-1.1 1.9-2.1 0-.2 0-.4-.1-.6l-1.3-4c-.1-.2 0-.2.1-.3m-7.6 5.7c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m-5 3c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s.1 2 1 2.4m-3.2 4.8c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m14.8 11c4.4 0 8.6.3 12.3.8 1.1-4.5 2.4-7 3.7-8.8l-2.5-.9c.2 1.3.3 1.9 0 2.7-.4-.4-.8-1.1-1.1-2.3l-1.2 4c.7-.5 1.3-.8 2-.9-1.1 2.5-2.6 3.1-3.5 3-1.1-.2-1.7-1.2-1.5-2.1.3-1.2 1.5-1.5 2.1-.1 1.1-2.3-.8-3-2-2.3 1.9-1.9 2.1-3.5.6-5.6-2.1 1.6-2.1 3.2-1.2 5.5-1.2-1.4-3.2-.6-2.5 1.6.9-1.4 2.1-.5 1.9.8-.2 1.1-1.7 2.1-3.5 1.9-2.7-.2-2.9-2.1-2.9-3.6.7-.1 1.9.5 2.9 1.9l.4-4.3c-1.1 1.1-2.1 1.4-3.2 1.4.4-1.2 2.1-3 2.1-3h-5.4s1.7 1.9 2.1 3c-1.1 0-2.1-.2-3.2-1.4l.4 4.3c1-1.4 2.2-2 2.9-1.9-.1 1.5-.2 3.4-2.9 3.6-1.9.2-3.4-.8-3.5-1.9-.2-1.3 1-2.2 1.9-.8.7-2.3-1.2-3-2.5-1.6.9-2.2.9-3.9-1.2-5.5-1.5 2-1.3 3.7.6 5.6-1.2-.7-3.1 0-2 2.3.6-1.4 1.8-1.1 2.1.1.2.9-.3 1.9-1.5 2.1-.9.2-2.4-.5-3.5-3 .6 0 1.2.3 2 .9l-1.2-4c-.3 1.1-.7 1.9-1.1 2.3-.3-.8-.2-1.4 0-2.7l-2.9.9C1.3 23 2.6 25.5 3.7 30c3.7-.5 7.9-.8 12.3-.8"></path>
</svg>
<!--<![endif]-->
<!--[if IE 8]>
<img src="/assets/images/govuk-logotype-tudor-crown.png" class="govuk-header__logotype-crown-fallback-image" width="32" height="30" alt="">
<![endif]-->
<span class="govuk-header__logotype-text">
GOV.UK
</span>
</span>
</a>
</div>
<div class="govuk-header__content">
<a asp-page="/oidc-test" class="govuk-header__link govuk-header__link--service-name">
OIDC test app
</a>
@if (User.Identity?.IsAuthenticated == true)
{
<nav aria-label="Menu" class="govuk-header__navigation">
<button type="button" class="govuk-header__menu-button govuk-js-header-toggle" aria-controls="navigation" hidden>
Menu
</button>
<ul id="navigation" class="govuk-header__navigation-list">
<li class="govuk-header__navigation-item govuk-header__navigation-item--active">
<a class="govuk-header__link" asp-page="SignOut">
Sign out
</a>
</li>
</ul>
</nav>
}
@RenderSection("Nav", required: false)
</div>
</div>
</header>
}

@if (User.Identity?.IsAuthenticated == true)
{
@section Nav {
<nav aria-label="Menu" class="govuk-header__navigation">
<button type="button" class="govuk-header__menu-button govuk-js-header-toggle" aria-controls="navigation" hidden>
Menu
</button>
<ul id="navigation" class="govuk-header__navigation-list">
<li class="govuk-header__navigation-item govuk-header__navigation-item--active">
<a class="govuk-header__link" asp-page="SignOut">
Sign out
</a>
</li>
</ul>
</nav>
}
@section BeforeContent {
@RenderSection("BeforeContent", required: false)
}

@RenderBody()

@section Footer {
<footer class="govuk-footer" role="contentinfo">
<div class="govuk-width-container ">
<div class="govuk-footer__meta">
<div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
</div>
<div class="govuk-footer__meta-item">
<a class="govuk-footer__link govuk-footer__copyright-logo" href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/"Crown copyright</a>
</div>
</div>
</div>
</footer>
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@{
Layout = "./ErrorLayout";
ViewBag.Title = "Sorry, there is a problem with the service";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@{
Layout = "./ErrorLayout";
ViewBag.Title = "Page not found";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@{
Layout = "./Layout";
}

@section Header {
<header class="govuk-header" role="banner" data-module="govuk-header">
<div class="govuk-header__container govuk-width-container">
<div class="govuk-header__logo">
<a href="https://www.gov.uk/" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<!--[if gt IE 8]><!-->
<svg aria-hidden="true"
focusable="false"
class="govuk-header__logotype-crown"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 30"
height="30"
width="32">
<path fill="currentColor" fill-rule="evenodd"
d="M22.6 10.4c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m-5.9 6.7c-.9.4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m10.8-3.7c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s0 2-1 2.4m3.3 4.8c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4M17 4.7l2.3 1.2V2.5l-2.3.7-.2-.2.9-3h-3.4l.9 3-.2.2c-.1.1-2.3-.7-2.3-.7v3.4L15 4.7c.1.1.1.2.2.2l-1.3 4c-.1.2-.1.4-.1.6 0 1.1.8 2 1.9 2.2h.7c1-.2 1.9-1.1 1.9-2.1 0-.2 0-.4-.1-.6l-1.3-4c-.1-.2 0-.2.1-.3m-7.6 5.7c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m-5 3c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s.1 2 1 2.4m-3.2 4.8c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m14.8 11c4.4 0 8.6.3 12.3.8 1.1-4.5 2.4-7 3.7-8.8l-2.5-.9c.2 1.3.3 1.9 0 2.7-.4-.4-.8-1.1-1.1-2.3l-1.2 4c.7-.5 1.3-.8 2-.9-1.1 2.5-2.6 3.1-3.5 3-1.1-.2-1.7-1.2-1.5-2.1.3-1.2 1.5-1.5 2.1-.1 1.1-2.3-.8-3-2-2.3 1.9-1.9 2.1-3.5.6-5.6-2.1 1.6-2.1 3.2-1.2 5.5-1.2-1.4-3.2-.6-2.5 1.6.9-1.4 2.1-.5 1.9.8-.2 1.1-1.7 2.1-3.5 1.9-2.7-.2-2.9-2.1-2.9-3.6.7-.1 1.9.5 2.9 1.9l.4-4.3c-1.1 1.1-2.1 1.4-3.2 1.4.4-1.2 2.1-3 2.1-3h-5.4s1.7 1.9 2.1 3c-1.1 0-2.1-.2-3.2-1.4l.4 4.3c1-1.4 2.2-2 2.9-1.9-.1 1.5-.2 3.4-2.9 3.6-1.9.2-3.4-.8-3.5-1.9-.2-1.3 1-2.2 1.9-.8.7-2.3-1.2-3-2.5-1.6.9-2.2.9-3.9-1.2-5.5-1.5 2-1.3 3.7.6 5.6-1.2-.7-3.1 0-2 2.3.6-1.4 1.8-1.1 2.1.1.2.9-.3 1.9-1.5 2.1-.9.2-2.4-.5-3.5-3 .6 0 1.2.3 2 .9l-1.2-4c-.3 1.1-.7 1.9-1.1 2.3-.3-.8-.2-1.4 0-2.7l-2.9.9C1.3 23 2.6 25.5 3.7 30c3.7-.5 7.9-.8 12.3-.8"></path>
</svg>
<!--<![endif]-->
<!--[if IE 8]>
<img src="/assets/images/govuk-logotype-tudor-crown.png" class="govuk-header__logotype-crown-fallback-image" width="32" height="30" alt="">
<![endif]-->
<span class="govuk-header__logotype-text">
GOV.UK
</span>
</span>
</a>
</div>
<div class="govuk-header__content">
<a href="/" class="govuk-header__link govuk-header__link--service-name">
Authorise access to a teaching record
</a>
</div>
</div>
</header>
}

@RenderBody()
Loading

0 comments on commit d113da9

Please sign in to comment.