Skip to content

CODE RUB: Implent NHS Login frontend and remove MSAL#209

Open
davidhayes03 wants to merge 5 commits into
mainfrom
users/davidhayes03/code-rub-loginFrontend
Open

CODE RUB: Implent NHS Login frontend and remove MSAL#209
davidhayes03 wants to merge 5 commits into
mainfrom
users/davidhayes03/code-rub-loginFrontend

Conversation

@davidhayes03
Copy link
Copy Markdown
Contributor

@davidhayes03 davidhayes03 commented Apr 23, 2026

CLOSES AB#28541

@davidhayes03 davidhayes03 requested review from cjdutoit and Copilot and removed request for Copilot April 23, 2026 11:46
@github-actions github-actions Bot added the CODE RUB The code rub category is for small changes (not for bug fixes) label Apr 23, 2026
Copilot AI review requested due to automatic review settings April 23, 2026 11:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR switches the Manage app’s authentication approach toward an NHS Login (CIS2) / BFF-cookie + server session model, while de-emphasizing the existing MSAL-based client flow.

Changes:

  • Added server-side CIS2 auth endpoints (/auth/login, /auth/callback, /auth/session, /auth/logout) and a new EF User store.
  • Introduced SQL-backed session caching + Data Protection key persistence, plus secure token/session storage helpers.
  • Updated the React client to use /auth/* endpoints and cookie-based session checks instead of MSAL templates/providers.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 25 comments.

Show a summary per file
File Description
LondonDataServices.IDecide.Manage.Server/appsettings.json Adds SessionCache connection string for SQL-backed session storage.
LondonDataServices.IDecide.Manage.Server/TokenResult.cs Adds DTO for CIS2 token refresh responses.
LondonDataServices.IDecide.Manage.Server/Services/TokenService.cs Adds refresh-token-based access token retrieval/refresh logic.
LondonDataServices.IDecide.Manage.Server/Services/SecureTokenStorage.cs Adds DataProtection-encrypted token + CSRF state storage in session.
LondonDataServices.IDecide.Manage.Server/Services/ITokenService.cs Introduces access-token retrieval abstraction.
LondonDataServices.IDecide.Manage.Server/Properties/launchSettings.json Alters HTTPS profile binding settings.
LondonDataServices.IDecide.Manage.Server/Program.cs Configures session, cookies, Data Protection, CIS2 SDK, and new DbContext.
LondonDataServices.IDecide.Manage.Server/Models/User.cs Adds EF model for CIS2 users and authorisation flag.
LondonDataServices.IDecide.Manage.Server/Models/NhsUserInfo.cs Adds CIS2 userinfo/NRBAC role models.
LondonDataServices.IDecide.Manage.Server/LondonDataServices.IDecide.Manage.Server.csproj Adds SQL cache + NHS Digital SDK package references.
LondonDataServices.IDecide.Manage.Server/Data/ApplicationDbContext .cs Adds EF DbContext for the new User table.
LondonDataServices.IDecide.Manage.Server/Controllers/AuthController.cs Implements login/callback/session/logout endpoints and cookie sign-in.
LondonDataServices.IDecide.Manage.Client/vite.config.ts Adds /auth/ proxy and changes dev-server port.
LondonDataServices.IDecide.Manage.Client/src/services/foundations/authService.ts Adds react-query hooks for session and logout.
LondonDataServices.IDecide.Manage.Client/src/models/sessions/SessionData.ts Adds client session model (includes expiresAt).
LondonDataServices.IDecide.Manage.Client/src/main.tsx Stops passing MSAL instance into App but still initializes MSAL.
LondonDataServices.IDecide.Manage.Client/src/hooks/useAuth.ts Adds cookie-session polling + expiry-based auto logout.
LondonDataServices.IDecide.Manage.Client/src/components/securitys/userProfile.tsx Switches profile display to server session data + expiry display.
LondonDataServices.IDecide.Manage.Client/src/components/root.tsx Replaces MSAL templates with session-based route guarding.
LondonDataServices.IDecide.Manage.Client/src/components/layouts/navbar.tsx Adds dropdown user menu and logout based on session data.
LondonDataServices.IDecide.Manage.Client/src/components/layouts/loginUnauth.tsx Switches sign-in to /auth/login.
LondonDataServices.IDecide.Manage.Client/src/brokers/apiBroker.ts Removes MSAL token injection; uses cookie credentials for requests.
LondonDataServices.IDecide.Manage.Client/src/brokers/apiBroker.authSession.ts Adds broker calls for /auth/session and /auth/logout.
LondonDataServices.IDecide.Manage.Client/src/App.tsx Removes MsalProvider wrapper and adds /unauthorised route.
Comments suppressed due to low confidence (1)

LondonDataServices.IDecide.Manage.Server/Program.cs:239

  • AddSwaggerGen() is registered twice and AddControllers() is also called twice in this method. This is redundant and can lead to confusing configuration ordering. Remove the duplicates so each service is registered once in a single place.
            builder.Services.AddSwaggerGen();
            builder.Services.AddScoped<ISecureTokenStorage, SecureTokenStorage>();
            builder.Services.AddScoped<ITokenService, TokenService>();
            builder.Services.AddSingleton(invisibleApiKey);
            builder.Services.AddAuthorization();
            builder.Services.AddDbContext<StorageBroker>();

            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    configuration.GetConnectionString("IDecideConnectionString")));

            builder.Services.AddHttpContextAccessor();
            builder.Services.AddHttpClient();

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            builder.Services.AddControllers();
            AddProviders(builder.Services, builder.Configuration);
            AddBrokers(builder.Services, builder.Configuration);
            AddFoundationServices(builder.Services);
            AddOrchestrationServices(builder.Services, builder.Configuration);
            AddClients(builder.Services);
            //  AddProcessingServices(builder.Services);
            //  AddCoordinationServices(builder.Services, builder.Configuration);

            // Register IConfiguration to be available for dependency injection
            builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
            JsonNamingPolicy jsonNamingPolicy = JsonNamingPolicy.CamelCase;

            builder.Services.AddControllers()
               .AddOData(options =>
               {
                   options.AddRouteComponents("odata", GetEdmModel());

Comment thread LondonDataServices.IDecide.Manage.Server/Controllers/AuthController.cs Outdated
Comment thread LondonDataServices.IDecide.Manage.Server/Services/SecureTokenStorage.cs Outdated
Comment thread LondonDataServices.IDecide.Manage.Server/Services/SecureTokenStorage.cs Outdated
Comment thread LondonDataServices.IDecide.Manage.Server/Program.cs
Comment thread LondonDataServices.IDecide.Manage.Client/src/main.tsx Outdated
Comment thread LondonDataServices.IDecide.Manage.Client/vite.config.ts
Comment thread LondonDataServices.IDecide.Manage.Server/Models/User.cs Outdated
Comment thread LondonDataServices.IDecide.Manage.Server/Services/TokenService.cs Outdated
Copy link
Copy Markdown
Contributor

@cjdutoit cjdutoit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REJECTED: Split this PR up. Need PR's for:

  1. Do a storage broker if you need a complete new DbContext with models and migration
  2. Do a PR for TokenService and add its unit tests and proper exception handling and validation
  3. Do a PR for SecureTokenStorageService. All lumped into one file and currently no tests
  4. Can't see where TokenStorage and Token Service is used, you probably need an orchestration service and a separate PR for the EXPOSER.
  5. All UI stuff in its own PR

{
[ApiController]
[Route("[controller]")]
public class AuthController : Controller
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing all the unit tests and security tests for this controller


public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have a seperate DbContext? Why not use the existing IDecide database.

Dont see any DI registration for this or connection strings or migrations


namespace LondonDataServices.IDecide.Manage.Server.Services
{
public class TokenService : ITokenService
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service implemented without unit tests

Copilot AI review requested due to automatic review settings April 23, 2026 13:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 20 comments.

Comments suppressed due to low confidence (1)

LondonDataServices.IDecide.Manage.Server/Program.cs:230

  • AddSwaggerGen() and AddControllers() are registered twice. This is redundant and can lead to confusing configuration if options get added in both places. Keep a single registration and consolidate any configuration into one call chain.
            builder.Services.AddSwaggerGen();
            //builder.Services.AddScoped<ISecureTokenStorage, SecureTokenStorage>();
            builder.Services.AddSingleton(invisibleApiKey);
            builder.Services.AddAuthorization();
            builder.Services.AddDbContext<StorageBroker>();

            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    configuration.GetConnectionString("IDecideConnectionString")));

            builder.Services.AddHttpContextAccessor();
            builder.Services.AddHttpClient();

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();
            builder.Services.AddControllers();
            AddProviders(builder.Services, builder.Configuration);
            AddBrokers(builder.Services, builder.Configuration);
            AddFoundationServices(builder.Services);
            AddOrchestrationServices(builder.Services, builder.Configuration);
            AddClients(builder.Services);

            // Register IConfiguration to be available for dependency injection
            builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
            JsonNamingPolicy jsonNamingPolicy = JsonNamingPolicy.CamelCase;

            builder.Services.AddControllers()
               .AddOData(options =>

Comment on lines +153 to +168
builder.Services.AddAuthentication("bff-cookie")
.AddCookie("bff-cookie", options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
options.LoginPath = "/Login";
options.LogoutPath = "/Logout";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.Name = "bff-cookie";
});
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cookie auth options point LoginPath/LogoutPath at /Login and /Logout, but this controller exposes /auth/login and /auth/logout. If anything triggers a challenge/sign-out using these paths (outside of the custom 401 handler), it will route to non-existent endpoints. Update the cookie options to match the actual auth routes.

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +112
this.logger.LogWarning("Authorization code is missing from callback");
return BadRequest("Authorization code is missing");
}

if (string.IsNullOrEmpty(state))
{
this.logger.LogWarning("State parameter is missing from callback");
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repository formatting rule: any return that is not the first statement in its block must be preceded by exactly one blank line. These return BadRequest(...) statements follow a log call without a blank line; insert a blank line before each return to match the codebase formatting rules.

Suggested change
this.logger.LogWarning("Authorization code is missing from callback");
return BadRequest("Authorization code is missing");
}
if (string.IsNullOrEmpty(state))
{
this.logger.LogWarning("State parameter is missing from callback");
this.logger.LogWarning("Authorization code is missing from callback");
return BadRequest("Authorization code is missing");
}
if (string.IsNullOrEmpty(state))
{
this.logger.LogWarning("State parameter is missing from callback");

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +20
export class SessionData {
sub: string;
upn: string;
name: string;
roles: string[];
expiresAt: string;

constructor(
sub: string,
upn: string,
name: string,
roles: string[],
expiresAt: string
) {
this.sub = sub;
this.upn = upn;
this.name = name;
this.roles = roles;
this.expiresAt = expiresAt;
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SessionData.expiresAt is modeled as a required field, but the current /auth/session response from the server does not include it. This will leave expiresAt undefined at runtime and break the expiry checks/UI. Make expiresAt optional (or add it to the backend response) to keep the types and runtime payload aligned.

Copilot uses AI. Check for mistakes.
HttpContext.Session.Clear();
await HttpContext.SignOutAsync("bff-cookie");

return Redirect(@"\");
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirect(@"\\") uses a backslash, which produces an unusual/invalid redirect target in browsers. Use a forward-slash root path ("/") (or return a proper logout result as discussed) so navigation is consistent across platforms.

Suggested change
return Redirect(@"\");
return Redirect("/");

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +175
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userInfo.Sub),
new Claim(ClaimTypes.Name, userInfo.Name),
new Claim(ClaimTypes.Upn, userInfo.NhsIdUserUid),
};

foreach (var role in userInfo.NhsIdNrbacRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role.RoleName));
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This controller adds ClaimTypes.Role values from userInfo.NhsIdNrbacRoles[*].RoleName, but the rest of the API enforces roles like LondonDataServices.IDecide.Manage.Server.Administrators via [Authorize(Roles = ...)]. Unless CIS2 role names exactly match those strings, every role-protected endpoint will return 403. Introduce a mapping from CIS2/NRBAC roles to the app’s existing role names (or update the authorization policies/attributes to match the new role strategy).

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17


Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two consecutive blank lines before the end of useGetSession. Remove the extra blank line to keep formatting consistent with the rest of the client code and avoid noisy diffs.

Suggested change

Copilot uses AI. Check for mistakes.
Comment on lines 4 to +71
@@ -24,6 +26,7 @@
using LondonDataServices.IDecide.Core.Brokers.DateTimes;
using LondonDataServices.IDecide.Core.Brokers.Identifiers;
using LondonDataServices.IDecide.Core.Brokers.Loggings;
using LondonDataServices.IDecide.Core.Brokers.NhsDigitalApi;
using LondonDataServices.IDecide.Core.Brokers.Notifications;
using LondonDataServices.IDecide.Core.Brokers.Pds;
using LondonDataServices.IDecide.Core.Brokers.Securities;
@@ -49,16 +52,23 @@
using LondonDataServices.IDecide.Core.Services.Orchestrations.Consumers;
using LondonDataServices.IDecide.Core.Services.Orchestrations.Decisions;
using LondonDataServices.IDecide.Core.Services.Orchestrations.Patients;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using LondonDataServices.IDecide.Manage.Server.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.Identity.Web;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using NHSDigital.ApiPlatform.Sdk;
using NHSDigital.ApiPlatform.Sdk.AspNetCore;
using NHSDigital.ApiPlatform.Sdk.Models.Configurations;
using static System.Collections.Specialized.BitVector32;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several using directives appear unused (System.Runtime.InteropServices, Microsoft.Identity.Client.Extensions.Msal, Microsoft.Identity.Web, and static System.Collections.Specialized.BitVector32). These will produce compiler warnings and make the entrypoint harder to read. Remove unused usings (and any unused locals like azureAdOptions) to keep the file clean.

Copilot uses AI. Check for mistakes.
HttpContext.Session.Clear();
await HttpContext.SignOutAsync("bff-cookie");

return Redirect(@"\");
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logout returns an HTTP redirect (and uses a backslash path), but the frontend logout broker expects a JSON body with a logoutUrl. This will cause response.json() to fail and logout to break. Align the contract: either return JSON from /auth/logout or update the client to follow redirects and not parse JSON.

Suggested change
return Redirect(@"\");
return Ok(new
{
logoutUrl = "/"
});

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +28
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

// Add your DbSets here
public DbSet<User> Users { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// Configure your entities here
modelBuilder.Entity<User>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.NhsIdUserUid).HasMaxLength(50).IsRequired();
entity.Property(e => e.Name).HasMaxLength(200).IsRequired();
entity.HasIndex(e => e.NhsIdUserUid).IsUnique();
});
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file introduces a second EF Core ApplicationDbContext + Manage.Server.Models.User, but the Core StorageBroker already has a Users DbSet and migrations for LondonDataServices.IDecide.Core.Models.Foundations.Users.User. Duplicating the model/context risks schema drift and migration-history conflicts, especially since both contexts target the same connection string. Prefer reusing the existing Core user model/context (or clearly separate databases/migration history tables if a second context is intentional).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CODE RUB The code rub category is for small changes (not for bug fixes) draft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants