Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reference DQT record to allow teachers with prohibitions to sign in #762

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ public AuthenticationState(
public bool? RequiresTrnVerificationLevelElevation { get; private set; }
public bool? TrnVerificationElevationSuccessful { get; set; }

public bool? Blocked { get; set; }

[JsonIgnore]
public bool EmailAddressSet => EmailAddress is not null;
[JsonIgnore]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class AuthorizationController : Controller
private readonly TeacherIdentityServerDbContext _dbContext;
private readonly IClock _clock;
private readonly TrnTokenHelper _trnTokenHelper;
private readonly UserHelper _userHelper;
private readonly IConfiguration _configuration;

public AuthorizationController(
Expand All @@ -37,6 +38,7 @@ public AuthorizationController(
TeacherIdentityServerDbContext dbContext,
IClock clock,
TrnTokenHelper trnTokenHelper,
UserHelper userHelper,
IConfiguration configuration)
{
_applicationManager = applicationManager;
Expand All @@ -47,6 +49,7 @@ public AuthorizationController(
_dbContext = dbContext;
_clock = clock;
_trnTokenHelper = trnTokenHelper;
_userHelper = userHelper;
_configuration = configuration;
}

Expand Down Expand Up @@ -544,18 +547,22 @@ private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal
return signedInUser;
}

private async Task<ClaimsPrincipal?> InitializeAuthenticationState(User? signedInUser, EnhancedTrnToken? trnToken,
private async Task<ClaimsPrincipal?> InitializeAuthenticationState(
User? signedInUser,
EnhancedTrnToken? trnToken,
AuthenticationState authenticationState)
{
if (trnToken is null)
{
authenticationState.OnSignedInUserProvided(signedInUser);
await CheckCanAccessService();
return null;
}

if (signedInUser is not null)
{
_trnTokenHelper.InitializeAuthenticationStateForSignedInUser(signedInUser, authenticationState, trnToken);
await CheckCanAccessService();
return null;
}

Expand All @@ -566,6 +573,7 @@ private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal
if (existingValidUser.Trn is null || existingValidUser.Trn == trnToken.Trn)
{
_trnTokenHelper.InitializeAuthenticationStateForExistingUser(existingValidUser, authenticationState, trnToken);
await CheckCanAccessService();
return await authenticationState.SignIn(HttpContext);
}
}
Expand All @@ -574,8 +582,17 @@ private static IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal
var existingAccountMatch = await _trnTokenHelper.GetExistingAccountMatchForToken(trnToken);
authenticationState.OnExistingAccountSearch(existingAccountMatch);
authenticationState.OnTrnTokenProvided(trnToken);
await CheckCanAccessService();
}

return null;

async Task CheckCanAccessService()
{
if (authenticationState.Trn is not null)
{
await _userHelper.CheckCanAccessService(authenticationState);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ protected virtual string Page(string pageName, string? handler = null, bool auth
return url;
}

public string Blocked() => Page("/SignIn/Blocked");

public string CompleteAuthorization() => Page("/SignIn/Complete");

public string Reset() => Page("/SignIn/Reset");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public override bool CanAccessStep(string step)
return step switch
{
Steps.Landing => true,
Steps.Blocked => AuthenticationState.Blocked == true,
SignInJourney.Steps.Email => true,
SignInJourney.Steps.EmailConfirmation => AuthenticationState is { EmailAddressSet: true, EmailAddressVerified: false },
Steps.NoAccount => AuthenticationState.EmailAddressVerified,
Expand Down Expand Up @@ -148,6 +149,7 @@ public override string GetLastAccessibleStepUrl(string? requestedStep)
SignInJourney.Steps.Email => LinkGenerator.Email(),
SignInJourney.Steps.EmailConfirmation => LinkGenerator.EmailConfirmation(),
Steps.Landing => LinkGenerator.Landing(),
Steps.Blocked => LinkGenerator.Blocked(),
Steps.Email => LinkGenerator.RegisterEmail(),
Steps.EmailConfirmation => LinkGenerator.RegisterEmailConfirmation(),
Steps.InstitutionEmail => LinkGenerator.RegisterInstitutionEmail(),
Expand Down Expand Up @@ -208,5 +210,6 @@ AuthenticationState is
public const string ChangeEmailRequest = $"{nameof(CoreSignInJourney)}.{nameof(ChangeEmailRequest)}";
public const string CheckAnswers = $"{nameof(CoreSignInJourney)}.{nameof(CheckAnswers)}";
public const string NoAccount = $"{nameof(CoreSignInJourney)}.{nameof(NoAccount)}";
public const string Blocked = $"{nameof(CoreSignInJourney)}.{nameof(Blocked)}";
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TeacherIdentity.AuthServer.Models;
Expand All @@ -12,6 +13,7 @@ public class CoreSignInJourneyWithTrnLookup : CoreSignInJourney
private readonly TrnLookupHelper _trnLookupHelper;
private readonly TeacherIdentityApplicationManager _applicationManager;
private readonly IBackgroundJobScheduler _backgroundJobScheduler;
private readonly ElevateTrnVerificationLevelJourney _elevateJourney;

public CoreSignInJourneyWithTrnLookup(
HttpContext httpContext,
Expand All @@ -25,10 +27,21 @@ public CoreSignInJourneyWithTrnLookup(
_trnLookupHelper = trnLookupHelper;
_applicationManager = applicationManager;
_backgroundJobScheduler = backgroundJobScheduler;

_elevateJourney = new ElevateTrnVerificationLevelJourney(trnLookupHelper, httpContext, linkGenerator, userHelper);
}

public override async Task<IActionResult> CreateUser(string currentStep)
{
await UserHelper.CheckCanAccessService(AuthenticationState);
Debug.Assert(AuthenticationState.Blocked.HasValue);

if (AuthenticationState.Blocked == true)
{
var nextPageUrl = GetStepUrl(CoreSignInJourney.Steps.Blocked);
return new RedirectResult(nextPageUrl);
}

using var suppressUniqueIndexViolationScope = SentryErrors.Suppress<DbUpdateException>(ex => ex.IsUniqueIndexViolation(User.TrnUniqueIndexName));
try
{
Expand Down Expand Up @@ -129,7 +142,7 @@ public override bool IsCompleted()
public override string GetNextStepUrl(string currentStep) =>
currentStep switch
{
ElevateTrnVerificationLevelJourney.Steps.Landing => ElevateTrnVerificationLevelJourney.GetStartStepUrl(LinkGenerator),
ElevateTrnVerificationLevelJourney.Steps.Landing => _elevateJourney.GetStartStepUrl(),
_ => base.GetNextStepUrl(currentStep)
};

Expand All @@ -139,7 +152,7 @@ public override string GetNextStepUrl(string currentStep) =>
// but the user's TrnVerificationLevel is Low (or null) we need to switch to the 'elevate' journey
if (IsFinished() && AuthenticationState.RequiresTrnVerificationLevelElevation == true)
{
return ElevateTrnVerificationLevelJourney.GetStartStepUrl(LinkGenerator);
return _elevateJourney.GetStartStepUrl();
}

var shouldCheckAnswers = (AreAllQuestionsAnswered() || FoundATrn) && !AuthenticationState.ExistingAccountFound;
Expand Down Expand Up @@ -209,7 +222,7 @@ public override string GetLastAccessibleStepUrl(string? requestedStep) =>
_ => base.GetLastAccessibleStepUrl(requestedStep)
};

public override Task<IActionResult> OnEmailVerified(User? user, string currentStep)
public override async Task<IActionResult> OnEmailVerified(User? user, string currentStep)
{
if (user is not null && user.UserType == Models.UserType.Default && user.TrnLookupStatus is null)
{
Expand All @@ -218,7 +231,19 @@ public override Task<IActionResult> OnEmailVerified(User? user, string currentSt
throw new NotImplementedException("Cannot lookup a TRN for an existing user.");
}

return base.OnEmailVerified(user, currentStep);
var result = await base.OnEmailVerified(user, currentStep);

if (user is not null)
{
await UserHelper.CheckCanAccessService(AuthenticationState);

if (AuthenticationState.Blocked == true)
{
return new RedirectResult(GetStepUrl(CoreSignInJourney.Steps.Blocked));
}
}

return result;
}

protected override bool AreAllQuestionsAnswered() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ public ElevateTrnVerificationLevelJourney(
_trnLookupHelper = trnLookupHelper;
}

public static string GetStartStepUrl(IdentityLinkGenerator linkGenerator) => linkGenerator.ElevateLanding();

public async Task LookupTrn()
{
var trn = await _trnLookupHelper.LookupTrn(AuthenticationState);
Expand All @@ -41,6 +39,7 @@ public async Task LookupTrn()
CoreSignInJourneyWithTrnLookup.Steps.NiNumber => true,
CoreSignInJourneyWithTrnLookup.Steps.Trn => AuthenticationState.HasNationalInsuranceNumber.HasValue,
Steps.CheckAnswers => AuthenticationState.HasNationalInsuranceNumber.HasValue && AuthenticationState.StatedTrn is not null,
CoreSignInJourney.Steps.Blocked => AuthenticationState.Blocked == true,
_ => false
};

Expand All @@ -60,10 +59,12 @@ public async Task LookupTrn()
_ => null
};

protected override string GetStartStep() => Steps.Landing;
protected override string GetStartStep() =>
AuthenticationState.Blocked == true ? CoreSignInJourney.Steps.Blocked : Steps.Landing;

protected override string GetStepUrl(string step) => step switch
{
CoreSignInJourney.Steps.Blocked => LinkGenerator.Blocked(),
Steps.Landing => LinkGenerator.ElevateLanding(),
CoreSignInJourneyWithTrnLookup.Steps.NiNumber => LinkGenerator.RegisterNiNumber(),
CoreSignInJourneyWithTrnLookup.Steps.Trn => LinkGenerator.RegisterTrn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private async Task SignInExistingUser(User user)
{
if (user.Trn is not null)
{
await UserHelper.EnsureDqtUserNameMatch(user, AuthenticationState);
await UserHelper.SyncStateFromDqtRecord(user, AuthenticationState);
}
await AuthenticationState.SignIn(HttpContext);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using TeacherIdentity.AuthServer.Models;

Expand All @@ -18,6 +19,16 @@ public TrnTokenSignInJourney(

public override async Task<IActionResult> CreateUser(string currentStep)
{
await UserHelper.CheckCanAccessService(AuthenticationState);
Debug.Assert(AuthenticationState.Blocked.HasValue);

if (AuthenticationState.Blocked == true)
{
var nextPageUrl = GetNextStepUrl(currentStep);
Debug.Assert(nextPageUrl == GetStepUrl(CoreSignInJourney.Steps.Blocked));
return new RedirectResult(nextPageUrl);
}

var user = await UserHelper.CreateUserWithTrnToken(AuthenticationState);

AuthenticationState.OnUserRegistered(user);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using Azure.Storage.Blobs;
Expand Down Expand Up @@ -261,17 +262,43 @@ public async Task CreateTrnResolutionZendeskTicket(
await _dbContext.SaveChangesAsync();
}

public async Task EnsureDqtUserNameMatch(User user, AuthenticationState authenticationState)
public async Task SyncStateFromDqtRecord(User user, AuthenticationState authenticationState)
{
var dqtUser = await _dqtApiClient.GetTeacherByTrn(user.Trn!);
Debug.Assert(user.Trn is not null);
var dqtUser = await _dqtApiClient.GetTeacherByTrn(user.Trn!) ??
throw new Exception($"Failed to lookup teacher by TRN: '{user.Trn}'.");

if (await CheckDqtTeacherRecordIsValid(dqtUser) &&
if (await CheckDqtTeacherRecordHasValidNames(dqtUser) &&
NameHelper.GetFullName(user.FirstName, user.MiddleName, user.LastName) !=
NameHelper.GetFullName(dqtUser!.FirstName, dqtUser.MiddleName, dqtUser.LastName))
{
await AssignDqtUserName(user.UserId, dqtUser);
await SyncNameFromDqtRecord(user.UserId, dqtUser);
authenticationState.OnNameSet(dqtUser.FirstName, dqtUser.MiddleName, dqtUser.LastName);
}

CheckCanAccessService(authenticationState, dqtUser);
}

public async Task CheckCanAccessService(AuthenticationState authenticationState)
{
if (!(authenticationState is { Trn: not null, OAuthState: { TrnRequirementType: TrnRequirementType.Required, BlockProhibitedTeachers: true } }))
{
authenticationState.Blocked = false;
return;
}

var dqtUser = await _dqtApiClient.GetTeacherByTrn(authenticationState.Trn!) ??
throw new Exception($"Failed to lookup teacher by TRN: '{authenticationState.Trn}'.");

CheckCanAccessService(authenticationState, dqtUser);
}

private void CheckCanAccessService(AuthenticationState authenticationState, TeacherInfo teacherInfo)
{
authenticationState.Blocked =
authenticationState is { Trn: not null, OAuthState: { TrnRequirementType: TrnRequirementType.Required, BlockProhibitedTeachers: true } } &&
teacherInfo.Alerts.Any(a => a.AlertType == AlertType.Prohibition) &&
!teacherInfo.AllowIdSignInWithProhibitions;
}

public async Task ElevateTrnVerificationLevel(Guid userId, string trn, string nationalInsuranceNumber)
Expand Down Expand Up @@ -323,7 +350,7 @@ public async Task SetNationalInsuranceNumber(Guid userId, string? nationalInsura
}
}

private async Task AssignDqtUserName(Guid userId, TeacherInfo dqtUser)
private async Task SyncNameFromDqtRecord(Guid userId, TeacherInfo dqtUser)
{
var existingUser = await _dbContext.Users.SingleAsync(u => u.UserId == userId);

Expand All @@ -348,13 +375,8 @@ private async Task AssignDqtUserName(Guid userId, TeacherInfo dqtUser)
await _dbContext.SaveChangesAsync();
}

private async Task<bool> CheckDqtTeacherRecordIsValid(TeacherInfo? teacher)
private async Task<bool> CheckDqtTeacherRecordHasValidNames(TeacherInfo teacher)
{
if (teacher is null)
{
return false;
}

if (string.IsNullOrEmpty(teacher.FirstName) || string.IsNullOrEmpty(teacher.LastName))
{
try
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/sign-in/blocked"
@inject IConfiguration Configuration
@model TeacherIdentity.AuthServer.Pages.SignIn.BlockedModel
@{
ViewBag.Title = $"You cannot {Model.ClientName}";
}

<div class="govuk-panel app-panel--interruption" data-testid="landing-panel">
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-l">@ViewBag.Title</h1>

<p>
Email <a href="mailto:@(Configuration["SupportEmail"])">@(Configuration["SupportEmail"])</a> to find out how to @Model.ClientName.
</p>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using TeacherIdentity.AuthServer.Journeys;
using TeacherIdentity.AuthServer.Oidc;
using TeacherIdentity.AuthServer.State;

namespace TeacherIdentity.AuthServer.Pages.SignIn;

[CheckCanAccessStep(CoreSignInJourney.Steps.Blocked)]
[AllowCompletedAuthenticationJourney]
public class BlockedModel : PageModel
{
private readonly ICurrentClientProvider _currentClientProvider;

public BlockedModel(ICurrentClientProvider currentClientProvider)
{
_currentClientProvider = currentClientProvider;
}

public string? ClientName { get; set; }

public async Task OnGet()
{
var client = await _currentClientProvider.GetCurrentClient();
ClientName = client!.DisplayName;
}
}
Loading
Loading