Skip to content

Commit

Permalink
Return verified names & birthdates from userinfo endpoint (#1286)
Browse files Browse the repository at this point in the history
  • Loading branch information
gunndabad authored Apr 16, 2024
1 parent f7a22c7 commit f031e84
Show file tree
Hide file tree
Showing 11 changed files with 2,175 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public static class ClaimTypes
public const string Subject = "sub";
public const string Trn = "trn";
public const string OneLoginIdToken = "onelogin_id";
public const string OneLoginVerifiedNames = "onelogin_verified_names";
public const string OneLoginIdVerifiedBirthDates = "onelogin_verified_birthdates";
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,33 @@ public async Task<IActionResult> Token()
[HttpGet("~/oauth2/userinfo")]
[HttpPost("~/oauth2/userinfo")]
[Produces("application/json")]
public IActionResult UserInfo()
public async Task<IActionResult> UserInfo()
{
var subject = User.GetClaim(ClaimTypes.Subject)!;

var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[ClaimTypes.Subject] = User.GetClaim(ClaimTypes.Subject)!,
[ClaimTypes.Trn] = User.GetClaim(ClaimTypes.Trn)!
[ClaimTypes.Subject] = subject
};

var oneLoginUser = await dbContext.OneLoginUsers
.Include(o => o.Person)
.SingleAsync(u => u.Subject == subject);

if (oneLoginUser.Person?.Trn is string trn)
{
claims.Add(ClaimTypes.Trn, trn);
}

if (User.HasScope(Scopes.Email))
{
claims[ClaimTypes.Email] = User.GetClaim(ClaimTypes.Email)!;
claims.Add(ClaimTypes.Email, oneLoginUser.Email);
}

if (oneLoginUser.VerificationRoute == OneLoginUserVerificationRoute.OneLogin)
{
claims.Add(ClaimTypes.OneLoginVerifiedNames, oneLoginUser.VerifiedNames!);
claims.Add(ClaimTypes.OneLoginIdVerifiedBirthDates, oneLoginUser.VerifiedDatesOfBirth!);
}

return Ok(claims);
Expand All @@ -151,7 +167,7 @@ private static IEnumerable<string> GetDestinations(Claim claim)
yield break;

case ClaimTypes.Email:
if (claim.Subject!.HasScope(Scopes.Profile))
if (claim.Subject!.HasScope(Scopes.Email))
{
yield return Destinations.IdentityToken;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using GovUk.OneLogin.AspNetCore;
using Microsoft.AspNetCore.Authentication;
Expand Down Expand Up @@ -179,12 +180,10 @@ void IConfigureNamedOptions<OneLoginOptions>.Configure(string? name, OneLoginOpt
// This handles the scenario where we've requested ID verification but One Login couldn't do it.

if (context.Properties!.TryGetVectorOfTrust(out var vtr) && vtr == SignInJourneyHelper.AuthenticationAndIdentityVerificationVtr &&
context.Properties?.Items.TryGetValue(PropertyKeys.JourneyInstanceId, out var serializedInstanceId) == true && serializedInstanceId is not null)
TryGetJourneyInstanceId(context.Properties, out var journeyInstanceId))
{
context.HandleResponse();

var journeyInstanceId = JourneyInstanceId.Deserialize(serializedInstanceId);

var signInJourneyHelper = context.HttpContext.RequestServices.GetRequiredService<SignInJourneyHelper>();
var journeyInstance = (await signInJourneyHelper.UserInstanceStateProvider.GetSignInJourneyInstanceAsync(context.HttpContext, journeyInstanceId))!;

Expand Down Expand Up @@ -221,6 +220,21 @@ void IConfigureNamedOptions<OneLoginOptions>.Configure(string? name, OneLoginOpt
options.NonceCookie.Name = "onelogin-nonce.";

static string EnsurePrefixedWithSlash(string value) => !value.StartsWith('/') ? "/" + value : value;

static bool TryGetJourneyInstanceId(
AuthenticationProperties? properties,
[NotNullWhen(true)] out JourneyInstanceId? journeyInstanceId)
{
if (properties?.Items.TryGetValue(PropertyKeys.JourneyInstanceId, out var serializedInstanceId) == true
&& serializedInstanceId is not null)
{
journeyInstanceId = JourneyInstanceId.Deserialize(serializedInstanceId);
return true;
}

journeyInstanceId = default;
return false;
}
}

void IConfigureOptions<OneLoginOptions>.Configure(OneLoginOptions options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,14 @@ async Task OnUserVerified()
{
var verifiedNames = ticket.Principal.GetCoreIdentityNames().Select(n => n.NameParts.Select(part => part.Value).ToArray()).ToArray();
var verifiedDatesOfBirth = ticket.Principal.GetCoreIdentityBirthDates().Select(d => d.Value).ToArray();
var coreIdentityClaimVc = ticket.Principal.FindFirstValue("vc") ?? throw new InvalidOperationException("No vc claim present.");

var oneLoginUser = await dbContext.OneLoginUsers.SingleAsync(u => u.Subject == sub);
oneLoginUser.VerifiedOn = clock.UtcNow;
oneLoginUser.VerificationRoute = OneLoginUserVerificationRoute.OneLogin;
oneLoginUser.VerifiedNames = verifiedNames;
oneLoginUser.VerifiedDatesOfBirth = verifiedDatesOfBirth;
oneLoginUser.LastCoreIdentityVc = coreIdentityClaimVc;
await dbContext.SaveChangesAsync();

await journeyInstance.UpdateStateAsync(state =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Filters;
using static IdentityModel.OidcConstants;
Expand Down Expand Up @@ -30,12 +34,15 @@ public static WebApplicationBuilder AddTestApp(this WebApplicationBuilder builde
options.ResponseMode = ResponseModes.Query;
options.ResponseType = ResponseTypes.Code;
options.MapInboundClaims = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("email");
options.Scope.Add("profile");
options.Scope.Add(CustomScopes.TeachingRecord);
options.ClaimActions.Add(new MapJsonClaimAction(ClaimTypes.OneLoginVerifiedNames));
options.ClaimActions.Add(new MapJsonClaimAction(ClaimTypes.OneLoginIdVerifiedBirthDates));
});
}
else
Expand All @@ -48,3 +55,14 @@ public static WebApplicationBuilder AddTestApp(this WebApplicationBuilder builde
return builder;
}
}

file class MapJsonClaimAction(string claimType) : ClaimAction(claimType, JsonClaimValueTypes.Json)
{
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
{
if (userData.TryGetProperty(ClaimType, out var element))
{
identity.AddClaim(new Claim(ClaimType, element.ToString(), valueType: ""));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class OneLoginUserMapping : IEntityTypeConfiguration<OneLoginUser>
public void Configure(EntityTypeBuilder<OneLoginUser> builder)
{
builder.HasKey(o => o.Subject);
builder.Property(o => o.Subject).HasMaxLength(200);
builder.Property(o => o.Subject).HasMaxLength(255);
builder.Property(o => o.Email).HasMaxLength(200);
builder.HasOne<Person>(o => o.Person).WithOne().HasForeignKey<OneLoginUser>(o => o.PersonId);
builder.Property(o => o.VerifiedNames).HasColumnType("jsonb").HasConversion<string>(
Expand All @@ -26,6 +26,7 @@ public void Configure(EntityTypeBuilder<OneLoginUser> builder)
new ValueComparer<DateOnly[]>(
(a, b) => (a == null && b == null) || (a != null && b != null && a.SequenceEqual(b)),
v => HashCode.Combine(v)));
builder.Property(o => o.LastCoreIdentityVc).HasColumnType("jsonb");
}
}

Expand Down
Loading

0 comments on commit f031e84

Please sign in to comment.