Skip to content

Commit

Permalink
Support trn_token in sign in journeys (#1292)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Apr 23, 2024
1 parent 8750f9f commit cdb8acb
Show file tree
Hide file tree
Showing 31 changed files with 674 additions and 194 deletions.
3 changes: 2 additions & 1 deletion TeachingRecordSystem/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.1.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.7.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
Expand Down Expand Up @@ -91,4 +92,4 @@
<PackageVersion Include="Xunit.DependencyInjection" Version="8.9.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,19 @@ 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 trnToken = parameters.GroupBy(kvp => kvp.Key).FirstOrDefault(kvp => kvp.Key == "trn_token")?.Select(kvp => kvp.Value).FirstOrDefault();

var authenticationProperties = new AuthenticationProperties()
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters),
Items =
{
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.OneLoginAuthenticationScheme, client.OneLoginAuthenticationSchemeName },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceName, client.Name },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceUrl, serviceUrl },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.TrnToken, trnToken },
}
};
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,30 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace TeachingRecordSystem.AuthorizeAccess;

public class IdDbContext(DbContextOptions<IdDbContext> options) : DbContext(options)
{
public DbSet<IdTrnToken> TrnTokens => Set<IdTrnToken>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<IdTrnToken>().HasKey(t => t.TrnToken);
}
}

[Table("trn_tokens")]
public class IdTrnToken
{
[Column("trn_token")]
public required string TrnToken { get; set; }
[Column("trn")]
public required string Trn { get; set; }
[Column("email")]
public required string Email { get; set; }
[Column("created_utc")]
public required DateTime CreatedUtc { get; set; }
[Column("expires_utc")]
public required DateTime ExpiresUtc { get; set; }
[Column("user_id")]
public Guid? UserId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public async Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? pr

var ticket = new AuthenticationTicket(user, properties, _scheme.Name);

var result = await helper.OnSignedInWithOneLogin(journeyInstance, ticket);
var result = await helper.OnOneLoginCallback(journeyInstance, ticket);

// Override the redirect done by RemoteAuthenticationHandler
_context.Response.OnStarting(() => result.ExecuteAsync(_context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,28 @@ public async Task ChallengeAsync(AuthenticationProperties? properties)
EnsureInitialized();

if (properties is null ||
!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)
!TryGetNonNullItem(AuthenticationPropertiesItemKeys.OneLoginAuthenticationScheme, out var oneLoginAuthenticationScheme) ||
!TryGetNonNullItem(AuthenticationPropertiesItemKeys.ServiceName, out var serviceName) ||
!TryGetNonNullItem(AuthenticationPropertiesItemKeys.ServiceUrl, out var serviceUrl))
{
throw new InvalidOperationException($"{nameof(AuthenticationProperties)} is missing one or more items.");
}

properties.Items.TryGetValue(AuthenticationPropertiesItemKeys.TrnToken, out var trnToken);

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

var result = helper.SignInWithOneLogin(journeyInstance);
await result.ExecuteAsync(_context);

bool TryGetNonNullItem(string key, [NotNullWhen(true)] out string? value)
{
value = default;
return properties?.Items.TryGetValue(key, out value) == true && value is not null;
}
}

public Task ForbidAsync(AuthenticationProperties? properties)
Expand Down Expand Up @@ -80,5 +85,6 @@ public static class AuthenticationPropertiesItemKeys
public const string OneLoginAuthenticationScheme = "OneLoginAuthenticationScheme";
public const string ServiceName = "ServiceName";
public const string ServiceUrl = "ServiceUrl";
public const string TrnToken = "TrnToken";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ void IConfigureNamedOptions<OneLoginOptions>.Configure(string? name, OneLoginOpt
var signInJourneyHelper = context.HttpContext.RequestServices.GetRequiredService<SignInJourneyHelper>();
var journeyInstance = (await signInJourneyHelper.UserInstanceStateProvider.GetSignInJourneyInstanceAsync(context.HttpContext, journeyInstanceId))!;

var result = await signInJourneyHelper.OnUserVerificationWithOneLoginFailed(journeyInstance);
var result = await signInJourneyHelper.OnVerificationFailed(journeyInstance);
await result.ExecuteAsync(context.HttpContext);
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
@page "/debug"
@model TeachingRecordSystem.AuthorizeAccess.Pages.DebugIdentityModel
@{
ViewBag.Title = "Identity information";
ViewBag.Title = "Debug One Login journey";
}

<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<form action="@LinkGenerator.DebugIdentity(Model.JourneyInstance!.InstanceId)" method="post">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>

<govuk-input asp-for="TrnToken" disabled="true" />

<h2 class="govuk-heading-m">User information</h2>

<govuk-input asp-for="Subject" disabled="true" />

<govuk-input asp-for="Email" type="email" disabled="true" />
Expand Down Expand Up @@ -43,7 +47,7 @@
<govuk-summary-list-row-value>@Model.Person.DateOfBirth?.ToString("dd/MM/yyyy")</govuk-summary-list-row-value>
</govuk-summary-list-row>
<govuk-summary-list-row>
<govuk-summary-list-row-key>National insurance number</govuk-summary-list-row-key>
<govuk-summary-list-row-key>National Insurance number</govuk-summary-list-row-key>
<govuk-summary-list-row-value>@Model.Person.NationalInsuranceNumber</govuk-summary-list-row-value>
</govuk-summary-list-row>
</govuk-summary-list>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ namespace TeachingRecordSystem.AuthorizeAccess.Pages;
public class DebugIdentityModel(
TrsDbContext dbContext,
SignInJourneyHelper helper,
IClock clock,
IOptions<AuthorizeAccessOptions> optionsAccessor) : PageModel
{
private OneLoginUser? _oneLoginUser;

public JourneyInstance<SignInJourneyState>? JourneyInstance { get; set; }

[Display(Name = "TRN token")]
public string? TrnToken { get; set; }

[Display(Name = "Subject")]
public string? Subject { get; set; }

Expand Down Expand Up @@ -106,49 +108,36 @@ public async Task<IActionResult> OnPost()
return this.PageWithErrors();
}

if (DetachPerson && _oneLoginUser!.PersonId is not null)
if (_oneLoginUser!.PersonId is not null && !DetachPerson)
{
await JourneyInstance!.UpdateStateAsync(state => helper.Complete(state, _oneLoginUser.Person!.Trn!));
return GetNextPage();
}

if (_oneLoginUser!.PersonId is not null && DetachPerson)
{
_oneLoginUser.PersonId = null;
}

if (_oneLoginUser!.PersonId is null)
if (IdentityVerified)
{
if (IdentityVerified)
{
_oneLoginUser!.VerifiedOn = clock.UtcNow;
_oneLoginUser.VerificationRoute = OneLoginUserVerificationRoute.OneLogin;
_oneLoginUser.VerifiedNames = verifiedNames;
_oneLoginUser.VerifiedDatesOfBirth = verifiedDatesOfBirth;
}
else
{
_oneLoginUser!.VerifiedOn = null;
_oneLoginUser.VerificationRoute = null;
_oneLoginUser.VerifiedNames = null;
_oneLoginUser.VerifiedDatesOfBirth = null;
}
await helper.OnUserVerifiedCore(JourneyInstance!, verifiedNames!, verifiedDatesOfBirth!, coreIdentityClaimVc: null);
}
else
{
_oneLoginUser!.VerifiedOn = null;
_oneLoginUser.VerificationRoute = null;
_oneLoginUser.VerifiedNames = null;
_oneLoginUser.VerifiedDatesOfBirth = null;

await JourneyInstance!.UpdateStateAsync(state => state.ClearVerified());
}

await dbContext.SaveChangesAsync();

await JourneyInstance!.UpdateStateAsync(state =>
{
if (_oneLoginUser!.PersonId is not null)
{
helper.Complete(state, _oneLoginUser.Person!.Trn!);
}
else if (IdentityVerified)
{
state.SetVerified(verifiedNames!, verifiedDatesOfBirth!);
}
else
{
state.ClearVerified();
}
});
return GetNextPage();

var nextPage = helper.GetNextPage(JourneyInstance);
return nextPage.ToActionResult();
IActionResult GetNextPage() => helper.GetNextPage(JourneyInstance!).ToActionResult();
}

public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
Expand All @@ -165,6 +154,7 @@ public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingConte
return;
}

TrnToken = JourneyInstance.State.TrnToken;
Subject = User.FindFirstValue("sub");
Email = User.FindFirstValue("email");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
}

<form asp-page="OidcTest" method="post">
<govuk-input asp-for="TrnToken" />

<govuk-button type="submit" is-start-button="true">Start</govuk-button>
</form>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
Expand All @@ -6,14 +7,22 @@ namespace TeachingRecordSystem.AuthorizeAccess.Pages.OidcTest;

public class StartModel : PageModel
{
[BindProperty]
[Display(Name = "TRN token")]
public string? TrnToken { get; set; }

public void OnGet()
{
}

public IActionResult OnPost() => Challenge(
new AuthenticationProperties()
{
RedirectUri = Url.Page("SignedIn")
RedirectUri = Url.Page("SignedIn"),
Parameters =
{
{ "TrnToken", TrnToken }
}
},
TestAppConfiguration.AuthenticationSchemeName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public class TestModel : PageModel
[FromQuery(Name = "scheme")]
public string? AuthenticationScheme { get; set; }

[FromQuery(Name = "trn_token")]
public string? TrnToken { get; set; }

public IActionResult OnGet()
{
if (User.Identity?.IsAuthenticated != true)
Expand All @@ -24,7 +27,8 @@ public IActionResult OnGet()
{
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.OneLoginAuthenticationScheme, AuthenticationScheme },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceName, "Test service" },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceUrl, Request.GetEncodedUrl() }
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.ServiceUrl, Request.GetEncodedUrl() },
{ MatchToTeachingRecordAuthenticationHandler.AuthenticationPropertiesItemKeys.TrnToken, TrnToken },
},
RedirectUri = Request.GetEncodedUrl()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ static SecurityKey LoadKey(string configurationValue)
RetryPauseTime = TimeSpan.FromSeconds(1)
};
builder.Services.AddDefaultServiceClient(ServiceLifetime.Transient, _ => crmServiceClient.Clone());

builder.Services.AddDbContext<IdDbContext>(options => options.UseNpgsql(builder.Configuration.GetRequiredConnectionString("Id")));
}

builder.Services
Expand Down
Loading

0 comments on commit cdb8acb

Please sign in to comment.