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

DP-1035 Dynamic FTS return URLs for integration environment #1100

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 18 additions & 3 deletions Frontend/CO.CDP.OrganisationApp.Tests/FtsUrlServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,39 @@ namespace CO.CDP.OrganisationApp.Tests;

public class FtsUrlServiceTests
{
private readonly Mock<ISession> _sessionMock;
private readonly Mock<IConfiguration> _configurationMock;
private readonly Mock<ICookiePreferencesService> _cookiePreferencesService;
private readonly IFtsUrlService _service;

public FtsUrlServiceTests()
{
_sessionMock = new();
_configurationMock = new Mock<IConfiguration>();
_cookiePreferencesService = new Mock<ICookiePreferencesService>();
_configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/");
_service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object);
_service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object);
}

[Fact]
public void BuildUrl_WhenFtsServiceOriginSessionIsSet_BaseServiceUrlOriginShouldBeSessionValue()
{
_sessionMock.Setup(s => s.Get<string?>(Session.FtsServiceOrigin)).Returns("https://example1.com/");
_configurationMock.Setup(c => c["FtsService"]).Returns("https://example2.com/");
var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object);
CultureInfo.CurrentUICulture = new CultureInfo("en-GB");

var result = service.BuildUrl("test-endpoint");

result.Should().Be("https://example1.com/test-endpoint?language=en_GB&cookies_accepted=unknown");
}

[Fact]
public void Constructor_ShouldThrowException_WhenFtsServiceIsNotConfigured()
{
_configurationMock.Setup(c => c["FtsService"]).Returns((string?)null);

Action action = () => new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object);
Action action = () => new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object);

action.Should().Throw<InvalidOperationException>()
.WithMessage("FtsService is not configured.");
Expand All @@ -34,7 +49,7 @@ public void Constructor_ShouldThrowException_WhenFtsServiceIsNotConfigured()
public void BuildUrl_ShouldTrimTrailingSlashFromBaseServiceUrl()
{
_configurationMock.Setup(c => c["FtsService"]).Returns("https://example.com/");
var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object);
var service = new FtsUrlService(_configurationMock.Object, _cookiePreferencesService.Object, _sessionMock.Object);
var endpoint = "test-endpoint";
CultureInfo.CurrentUICulture = new CultureInfo("en-GB");

Expand Down
48 changes: 47 additions & 1 deletion Frontend/CO.CDP.OrganisationApp.Tests/Pages/OneLoginTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CO.CDP.OrganisationApp.Authentication;
using CO.CDP.OrganisationApp.Constants;
using CO.CDP.OrganisationApp.Models;
using CO.CDP.OrganisationApp.Pages;
using CO.CDP.OrganisationApp.WebApiClients;
Expand All @@ -9,7 +10,9 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.FeatureManagement;
using Moq;
using System.Security.Claims;

Expand All @@ -24,6 +27,8 @@ public class OneLoginTest
private readonly Mock<IOneLoginAuthority> oneLoginAuthorityMock = new();
private readonly Mock<IAuthenticationService> authService = new();
private readonly Mock<IAuthorityClient> authorityClientMock = new();
private readonly Mock<IFeatureManager> featureManagerMock = new();
private readonly Mock<IConfiguration> configMock = new();
private const string urn = "urn:fdc:gov.uk:2022:7wTqYGMFQxgukTSpSI2GodMwe9";

[Fact]
Expand All @@ -48,6 +53,45 @@ public async Task OnGetSignIn_WhenRedirectUrlProvides_ShouldReturnAuthChallangeW
.Which.Properties!.RedirectUri.Should().Be("/one-login/user-info?redirectUri=%2Forg%2F1");
}

[Fact]
public async Task OnGetSignIn_WhenOriginIsNotNull_ShouldSetSessionWithFtsServiceOriginKey()
{
configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com");
featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(true);
var origin = "http://example1.com";
var model = GivenOneLoginModel("sign-in");

var result = await model.OnGetAsync("/org/1", origin: origin);

sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, origin), Times.Once);
}

[Fact]
public async Task OnGetSignIn_WhenOriginIsNotListedInConfiguration_ShouldNotSetSessionWithFtsServiceOriginKey()
{
configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com");
featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(true);
var origin = "http://example3.com";
var model = GivenOneLoginModel("sign-in");

var result = await model.OnGetAsync("/org/1", origin: origin);

sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, origin), Times.Never);
}

[Fact]
public async Task OnGetSignIn_WhenAllowDynamicFtsOriginsFeatureIsDisabled_ShouldNotSetSessionWithFtsServiceOriginKey()
{
configMock.Setup(c => c["FtsServiceAllowedOrigins"]).Returns("http://example1.com,http://example2.com");
featureManagerMock.Setup(f => f.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins)).ReturnsAsync(false);

var model = GivenOneLoginModel("sign-in");

var result = await model.OnGetAsync("/org/1", origin: "http://example1.com");

sessionMock.Verify(s => s.Set(Session.FtsServiceOrigin, "origin"), Times.Never);
}

[Fact]
public async Task OnGetUserInfo_OnSuccessfulAuthentication_ShouldRetrieveUserProfile()
{
Expand Down Expand Up @@ -320,7 +364,9 @@ private OneLoginModel GivenOneLoginModel(string pageAction)
logoutManagerMock.Object,
oneLoginAuthorityMock.Object,
authorityClientMock.Object,
new Mock<ILogger<OneLoginModel>>().Object)
new Mock<ILogger<OneLoginModel>>().Object,
featureManagerMock.Object,
configMock.Object)
{ PageAction = pageAction };
}
}
1 change: 1 addition & 0 deletions Frontend/CO.CDP.OrganisationApp/Constants/FeatureFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace CO.CDP.OrganisationApp.Constants;
public static class FeatureFlags
{
public const string Consortium = "Consortium";
public const string AllowDynamicFtsOrigins = "AllowDynamicFtsOrigins";
}
8 changes: 6 additions & 2 deletions Frontend/CO.CDP.OrganisationApp/FtsUrlService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ public class FtsUrlService : IFtsUrlService
{
private readonly string _ftsService;
private readonly ICookiePreferencesService _cookiePreferencesService;
private readonly ISession _session;

public FtsUrlService(IConfiguration configuration, ICookiePreferencesService cookiePreferencesService)
public FtsUrlService(IConfiguration configuration, ICookiePreferencesService cookiePreferencesService, ISession session)
{
var ftsService = configuration["FtsService"] ?? throw new InvalidOperationException("FtsService is not configured.");
_session = session;
var ftsService = _session.Get<string?>(Session.FtsServiceOrigin)
?? configuration["FtsService"]
?? throw new InvalidOperationException("FtsService is not configured.");
_ftsService = ftsService.TrimEnd('/');
_cookiePreferencesService = cookiePreferencesService;
}
Expand Down
26 changes: 20 additions & 6 deletions Frontend/CO.CDP.OrganisationApp/Pages/OneLogin.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.FeatureManagement;
using System.Net;

namespace CO.CDP.OrganisationApp.Pages;
Expand All @@ -22,16 +23,18 @@ public class OneLoginModel(
ILogoutManager logoutManager,
IOneLoginAuthority oneLoginAuthority,
IAuthorityClient authorityClient,
ILogger<OneLoginModel> logger) : PageModel
ILogger<OneLoginModel> logger,
IFeatureManager featureManager,
IConfiguration config) : PageModel
{
[BindProperty(SupportsGet = true)]
public required string PageAction { get; set; }

public async Task<IActionResult> OnGetAsync(string? redirectUri = null)
public async Task<IActionResult> OnGetAsync(string? redirectUri = null, string? origin = null)
{
return PageAction.ToLower() switch
{
"sign-in" => SignIn(redirectUri),
"sign-in" => await SignIn(redirectUri, origin),
"user-info" => await UserInfo(redirectUri),
"sign-out" => await SignOut(),
_ => Redirect("/"),
Expand Down Expand Up @@ -68,8 +71,19 @@ public async Task<IActionResult> OnPostAsync(string logout_token)
return Page();
}

private IActionResult SignIn(string? redirectUri = null)
private async Task<ChallengeResult> SignIn(string? redirectUri = null, string? origin = null)
{
session.Remove(Session.FtsServiceOrigin);
if (!string.IsNullOrWhiteSpace(origin)
&& await featureManager.IsEnabledAsync(FeatureFlags.AllowDynamicFtsOrigins))
{
var allowedOrigins = config["FtsServiceAllowedOrigins"] ?? "";
if (allowedOrigins.Split(",", StringSplitOptions.RemoveEmptyEntries).Contains(origin))
{
session.Set(Session.FtsServiceOrigin, origin);
}
}

var uri = "/one-login/user-info";
if (Helper.ValidRelativeUri(redirectUri))
{
Expand All @@ -84,7 +98,7 @@ private async Task<IActionResult> UserInfo(string? redirectUri = null)
var userInfo = await httpContextAccessor.HttpContext!.AuthenticateAsync();
if (!userInfo.Succeeded)
{
return SignIn();
return await SignIn(redirectUri);
}

var urn = userInfo.Principal.FindFirst(JwtClaimTypes.Subject)?.Value;
Expand All @@ -93,7 +107,7 @@ private async Task<IActionResult> UserInfo(string? redirectUri = null)

if (urn == null)
{
return SignIn();
return await SignIn(redirectUri);
}
await logoutManager.RemoveAsLoggedOut(urn);

Expand Down
1 change: 1 addition & 0 deletions Frontend/CO.CDP.OrganisationApp/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class Session(IHttpContextAccessor httpContextAccessor) : ISession
public const string RegistrationDetailsKey = "RegistrationDetails";
public const string ConnectedPersonKey = "ConnectedPerson";
public const string ConsortiumKey = "Consortium";
public const string FtsServiceOrigin = "FtsServiceOrigin";

public T? Get<T>(string key)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"DiagnosticPage": {
"Enabled": false,
"Path": ""
}
},
"AllowDynamicFtsOrigins": true
}
}
4 changes: 3 additions & 1 deletion Frontend/CO.CDP.OrganisationApp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"DataSharingService": "",
"EntityVerificationService": "",
"FtsService": "",
"FtsServiceAllowedOrigins": "",
"OneLogin": {
"Authority": "",
"ClientId": "",
Expand Down Expand Up @@ -56,7 +57,8 @@
},
"SharedSessions": false,
"Consortium": false,
"ContentSecurityPolicy": true
"ContentSecurityPolicy": true,
"AllowDynamicFtsOrigins": false
},
"CompaniesHouse": {
"Url": "",
Expand Down
Loading