Skip to content

Commit 3cec90a

Browse files
VIA-87 AS Implement sessions using iron-session and nextjs middleware to secure routes
1 parent 59f4d9b commit 3cec90a

File tree

12 files changed

+161
-19
lines changed

12 files changed

+161
-19
lines changed

.env.local

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ VACCINATION_APP_URL=http://localhost:3000
1515

1616
# NHS Login
1717
NHS_LOGIN_URL=https://auth.sandpit.signin.nhs.uk
18-
NHS_LOGIN_CLIENT_ID=should-not-be-checked-in
18+
NHS_LOGIN_CLIENT_ID=client-id-to-be-replaced
1919
NHS_LOGIN_SCOPE='openid profile gp_registration_details'
20-
NHS_LOGIN_PRIVATE_KEY_FILE_PATH=
20+
NHS_LOGIN_PRIVATE_KEY_FILE_PATH=path-to-key-file-from-root-of-this-repo
21+
SESSION_SECRET=secret-to-encyrpt-sessions

package-lock.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@aws-sdk/client-ssm": "^3.777.0",
6060
"axios": "^1.8.4",
6161
"dotenv": "^16.4.7",
62+
"iron-session": "^8.0.4",
6263
"isomorphic-dompurify": "^2.22.0",
6364
"next": "15.2.4",
6465
"nhsuk-react-components": "^5.0.0",
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { NextRequest, NextResponse } from "next/server";
22
import { logger } from "@src/utils/logger";
33
import { getClientConfig } from "@src/utils/auth/get-client-config";
44
import * as client from "openid-client";
5+
import { getSession } from "@src/utils/auth/session";
56

67
const log = logger.child({ module: "callback-route" });
78

89
export async function GET(request: NextRequest) {
9-
// TODO: Check if the state we receive back with the callback is the same as the one in session?
10-
// TEMP CODE: Using the state from request, just until we have sessions in place
10+
const session = await getSession();
1111
const state = request.nextUrl.searchParams.get("state");
1212

1313
if (!state) {
@@ -26,24 +26,26 @@ export async function GET(request: NextRequest) {
2626
},
2727
);
2828

29-
console.log("Token Set:", tokenSet);
3029
const { access_token } = tokenSet;
3130
const claims = tokenSet.claims()!;
3231
const { sub } = claims;
33-
34-
// call userinfo endpoint to get user info
3532
const userinfo = await client.fetchUserInfo(
3633
clientConfig!,
3734
access_token,
3835
sub,
3936
);
4037

41-
console.log("User Info:", userinfo);
38+
session.isLoggedIn = true;
39+
session.access_token = access_token;
40+
session.state = state;
41+
session.userInfo = {
42+
sub: userinfo.sub,
43+
};
44+
45+
await session.save();
46+
47+
return NextResponse.redirect(request.nextUrl.origin);
4248
} catch (e) {
4349
log.error(e);
4450
}
45-
46-
return NextResponse.json({
47-
content: "Hello World",
48-
});
4951
}

src/app/api/auth/session/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defaultSession, getSession } from "@src/utils/auth/session";
2+
3+
export async function GET() {
4+
try {
5+
const session = await getSession();
6+
if (!session) {
7+
return Response.json({ defaultSession });
8+
}
9+
return Response.json({
10+
isLoggedIn: session.isLoggedIn,
11+
userInfo: session.userInfo,
12+
});
13+
} catch (e) {
14+
return Response.json({ error: e }, { status: 500 });
15+
}
16+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { NextRequest, NextResponse } from "next/server";
6-
import { GET } from "@src/app/auth/sso/route";
6+
import { GET } from "@src/app/api/auth/sso/route";
77
import { getAuthConfig } from "@src/utils/auth/get-auth-config";
88
import { getClientConfig } from "@src/utils/auth/get-client-config";
99
import * as client from "openid-client";
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as client from "openid-client";
77
const log = logger.child({ module: "sso-route" });
88

99
export async function GET(request: NextRequest) {
10-
console.log(request);
1110
const assertedLoginIdentity = request.nextUrl.searchParams.get(
1211
"assertedLoginIdentity",
1312
);
@@ -27,7 +26,6 @@ export async function GET(request: NextRequest) {
2726
asserted_login_identity: assertedLoginIdentity,
2827
};
2928
const redirectTo = client.buildAuthorizationUrl(clientConfig, parameters);
30-
// TODO: Save state to session so it can be reused in the /token call in /auth/callback route
3129

3230
return NextResponse.redirect(redirectTo);
3331
} catch (e) {

src/app/sso-error/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const SsoErrorPage = () => {
2+
return <div>Error occured during SSO. Please try again later.</div>;
3+
};
4+
5+
export default SsoErrorPage;

src/middleware.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getSession } from "@src/utils/auth/session";
3+
import { logger } from "@src/utils/logger";
4+
5+
const log = logger.child({ name: "middleware" });
6+
7+
export async function middleware(request: NextRequest) {
8+
const sessionCookie = await getSession();
9+
logger.info(sessionCookie, "session");
10+
11+
if (!sessionCookie || !sessionCookie.access_token) {
12+
log.error("Session cookie not found");
13+
return NextResponse.redirect(`${request.nextUrl.origin}/sso-error`);
14+
}
15+
16+
try {
17+
return NextResponse.next();
18+
} catch (error) {
19+
log.error(error, "Error occurred during redirection");
20+
return NextResponse.redirect(`${request.nextUrl.origin}/sso-error`);
21+
}
22+
}
23+
24+
export const config = {
25+
matcher: [
26+
/*
27+
* Apply middleware to all pages except:
28+
* 1. /api/* (exclude all API routes)
29+
* 2. /sso/* (exclude sso route used for initiating auth flow)
30+
* 2. /login (exclude the login page)
31+
* 3. /_next/* (exclude Next.js assets, e.g., /_next/static/*)
32+
*/
33+
"/((?!api|sso|_next/static|_next/image).*)",
34+
],
35+
};

src/utils/auth/get-auth-config.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ describe("getAuthConfig", () => {
1919
jest.clearAllMocks();
2020
});
2121
it("is constructed with values from configProvider", async () => {
22-
// TODO: VIA-87 22/04/24 Might have to change this, as we haven't decided on the way we'll handle storing/generating
23-
// the URL of the application
2422
jest.replaceProperty(process, "env", {
2523
...process.env,
2624
VACCINATION_APP_URL: mockVaccinationAppUrl,
@@ -30,7 +28,7 @@ describe("getAuthConfig", () => {
3028
audience: mockVaccinationAppUrl,
3129
client_id: mockNhsLoginClientId,
3230
scope: mockNhsLoginScope,
33-
redirect_uri: `${mockVaccinationAppUrl}/auth/callback`,
31+
redirect_uri: `${mockVaccinationAppUrl}/api/auth/callback`,
3432
response_type: "code",
3533
grant_type: "authorization_code",
3634
post_login_route: `${mockVaccinationAppUrl}`,

0 commit comments

Comments
 (0)