Skip to content

Commit 9c6391a

Browse files
authored
KFP-664 Replace session with JWT token authentication (#44)
* KFP-664 added redis store for auth sessions * KFP-664 fix after review * KFP-664 added more logs * KFP-664 changed approach and replaced redis with jwt token * KFP-664 fix after review * KFP-664 code cleanup * KFP-664 fix interface * KFP-664 fix after review * KFP-664 fix issue and refactoring
1 parent 5ba9a8b commit 9c6391a

File tree

25 files changed

+347
-216
lines changed

25 files changed

+347
-216
lines changed

CoreDeployable/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ Set up the environment by adding a `.env` file to the root folder (CoreDeployabl
1818

1919
| Variable | Description |
2020
|----------|-------------|
21-
| `COOKIE_SECRET` | Secret key for encrypting session data |
22-
| `SESSION_SECRET` | Secret key for encrypting auth session data |
21+
| `SESSION_SECRET` | Secret used for JWT token signing |
2322
| `AUTH_CONFIG_FILE_NAME` | Name for auth configuration file for each form |
2423
| `ALLOWED_ORIGIN` | Used for API endpoint CORS configuration |
2524
| `CLOUD_PROVIDER` | Cloud provider to use (must be 'aws' or 'azure') |
@@ -53,6 +52,7 @@ Set up the environment by adding a `.env` file to the root folder (CoreDeployabl
5352
| `LOG_LEVEL` | Log verbosity | info |
5453
| `MAX_ALLOWED_FILE_SIZE_TO_UPLOAD` | Maximum file upload size in MB | 100 |
5554
| `USE_LOCAL_DYNAMODB` | Use local DynamoDB instance | false |
55+
| `SKIP_AUTH_ISSUER_CHECK` | Skip issuer validation for authentication sessions. Can be enabled when using a single SAML configuration | false |
5656

5757
##### Available Log Levels
5858
- trace
@@ -66,7 +66,6 @@ Set up the environment by adding a `.env` file to the root folder (CoreDeployabl
6666

6767
##### AWS Configuration Example
6868
```
69-
COOKIE_SECRET='your-secure-cookie-secret'
7069
SESSION_SECRET='your-secure-session-secret'
7170
CLOUD_PROVIDER='aws'
7271
BUCKET_NAME='your-kfd-files-bucket'
@@ -84,7 +83,6 @@ FORM_SESSION_TABLE_NAME='Core_FormSessions_dev'
8483

8584
##### Azure Configuration Example
8685
```
87-
COOKIE_SECRET='your-secure-cookie-secret'
8886
SESSION_SECRET='your-secure-session-secret'
8987
CLOUD_PROVIDER='azure'
9088
AZURE_STORAGE_ACCOUNT='your-storage-account'

CoreDeployable/package-lock.json

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

CoreDeployable/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@
6464
"crypto-js": "^4.2.0",
6565
"dompurify": "^3.2.7",
6666
"dotenv": "^17.2.2",
67-
"express-session": "^1.18.2",
6867
"helmet": "^8.1.0",
6968
"jsdom": "^27.0.0",
69+
"jsonwebtoken": "^9.0.2",
7070
"nocache": "^4.0.0",
7171
"winston": "^3.13.1"
7272
},
@@ -77,7 +77,7 @@
7777
"@types/crypto-js": "^4.2.2",
7878
"@types/cucumber": "^6.0.1",
7979
"@types/express-serve-static-core": "^5.0.7",
80-
"@types/express-session": "^1.18.2",
80+
"@types/jsonwebtoken": "^9.0.10",
8181
"@types/lodash": "^4.17.20",
8282
"@types/passport": "^1.0.16",
8383
"@types/selenium-webdriver": "^4.1.24",
@@ -110,7 +110,6 @@
110110
"core-fcads",
111111
"dompurify",
112112
"dotenv",
113-
"express-session",
114113
"core-gcds",
115114
"core-govuk",
116115
"core-nhsuk",
@@ -119,6 +118,7 @@
119118
"core-wds",
120119
"helmet",
121120
"jsdom",
121+
"jsonwebtoken",
122122
"nocache",
123123
"winston"
124124
]

CoreDeployable/src/config/envConfig.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ const envConfig = {
8383
}
8484
return provider;
8585
},
86+
get skipAuthIssuerCheck(): boolean {
87+
return process.env.SKIP_AUTH_ISSUER_CHECK === 'true';
88+
},
8689
};
8790

8891
export default envConfig;

CoreDeployable/src/config/expressConfiguration.ts

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1+
import { AuthenticateOptions } from 'passport';
2+
import { JwtService } from '../services/JwtService.js';
3+
import { Profile } from '@node-saml/passport-saml';
14
import bodyParser from 'body-parser';
25
import cookieParser from 'cookie-parser';
3-
import envConfig from './envConfig.js';
46
import express from 'express';
57
import { getCloudServices } from '../container/CloudServicesRegistry.js';
68
import helmet from 'helmet';
79
import { logger } from 'core-runtime';
810
import nocache from 'nocache';
911
import passport from '../middlewares/ssoHandler.js';
1012
import { permissionsPolicy } from '../middlewares/permissionsPolicy.js';
11-
import session from 'express-session';
1213

13-
declare module 'express-session' {
14-
interface SessionData {
15-
returnTo: string;
16-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17-
passport: any;
18-
}
19-
}
14+
const authenticateOptions: AuthenticateOptions = {
15+
session: false,
16+
failureRedirect: '/',
17+
failureFlash: true,
18+
};
2019

2120
export const expressConfiguration = (app: express.Express) => {
2221
const storageUrl = getCloudServices().fileService.getStorageUrl();
@@ -52,43 +51,64 @@ export const expressConfiguration = (app: express.Express) => {
5251
app.use(bodyParser.json({ type: 'application/json' }));
5352
app.use(cookieParser());
5453
app.use(bodyParser.urlencoded({ extended: true }));
55-
56-
app.use(
57-
session({
58-
secret: envConfig.sessionSecret,
59-
resave: false,
60-
saveUninitialized: false,
61-
}),
62-
);
63-
64-
app.use(passport.initialize());
65-
app.use(passport.session());
6654

67-
app.get('/login', passport.authenticate('saml', { failureRedirect: '/', failureFlash: true }), function (_req, res) {
68-
res.redirect('/');
69-
});
55+
app.get(
56+
'/login',
57+
(req, _res, next) => {
58+
logger.debug(`SAML Login - Starting authentication flow`);
59+
logger.debug(`SAML Login - RelayState: ${req.query?.RelayState || 'none'}`);
60+
next();
61+
},
62+
passport.authenticate('saml', authenticateOptions),
63+
function (_req, res) {
64+
res.redirect('/');
65+
},
66+
);
7067

7168
app.post(
7269
'/login/callback',
73-
passport.authenticate('saml', {
74-
failureRedirect: '/',
75-
failureFlash: true,
76-
}),
70+
(req, _res, next) => {
71+
logger.debug(`SAML Callback - Starting processing`);
72+
logger.debug(`SAML Callback - RelayState: ${req.body?.RelayState || 'none'}`);
73+
next();
74+
},
75+
passport.authenticate('saml', authenticateOptions),
7776
function (req, res) {
78-
// Handle RelayState from either body or query parameters, with fallback to session
79-
const relayState = req.body?.RelayState || req.query?.RelayState || req.session?.returnTo;
80-
const redirectUrl = relayState ? decodeURIComponent(relayState) : '/';
81-
82-
logger.debug(`SAML callback - RelayState: ${relayState}, Redirect to: ${redirectUrl}`);
83-
logger.debug(`Request body:`, req.body);
84-
logger.debug(`Request query:`, req.query);
85-
logger.debug(`Session returnTo:`, req.session?.returnTo);
86-
87-
// Clear the returnTo from session after using it
88-
if (req.session?.returnTo) {
89-
delete req.session.returnTo;
77+
logger.info(`SAML Auth Success - Processing user data`);
78+
logger.debug(`SAML Auth Success - User object:`, {
79+
hasUser: !!req.user,
80+
userType: typeof req.user,
81+
userKeys: req.user ? Object.keys(req.user) : [],
82+
});
83+
84+
if (req.user) {
85+
const user = req.user as Profile;
86+
87+
try {
88+
logger.info(`SAML Callback - Creating JWT for user`);
89+
90+
const token = JwtService.createToken(user);
91+
JwtService.setTokenCookie(res, token);
92+
logger.info(`SAML Callback - JWT token created and cookie set successfully`);
93+
} catch (error) {
94+
logger.error(`SAML Callback - JWT creation failed:`, {
95+
error: error instanceof Error ? error.message : error,
96+
stack: error instanceof Error ? error.stack : undefined,
97+
user: user?.nameID || user?.email || 'unknown',
98+
});
99+
throw new Error(
100+
`SAML authentication failed: JWT token creation failed - ${error instanceof Error ? error.message : 'Unknown error'}`,
101+
);
102+
}
103+
} else {
104+
logger.error(`SAML Callback - No user object received from SAML authentication`);
105+
throw new Error('SAML authentication failed: No user object received from SAML provider');
90106
}
91-
107+
108+
const relayState = req.body?.RelayState;
109+
const redirectUrl = relayState ? decodeURIComponent(relayState) : '/';
110+
111+
logger.info(`SAML Callback - Final redirect: ${redirectUrl}`);
92112
res.redirect(redirectUrl);
93113
},
94114
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const SAML_CLAIM_TYPES = {
2+
NAME: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
3+
EMAIL_ADDRESS: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
4+
} as const;

0 commit comments

Comments
 (0)