Skip to content

Commit

Permalink
Merge pull request #5914 from espoon-voltti/use-node-saml
Browse files Browse the repository at this point in the history
Käytetään node-samlia suoraan ylimääräisen passport-saml -kerroksen sijaan
  • Loading branch information
Gekkio authored Nov 18, 2024
2 parents 8914cda + c22e0b1 commit 7a34576
Show file tree
Hide file tree
Showing 19 changed files with 404 additions and 417 deletions.
1 change: 0 additions & 1 deletion apigw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
},
"dependencies": {
"@node-saml/node-saml": "^5.0.0",
"@node-saml/passport-saml": "^5.0.0",
"axios": "^1.7.4",
"connect-redis": "^7.1.0",
"cookie-parser": "^1.4.6",
Expand Down
32 changes: 17 additions & 15 deletions apigw/src/enduser/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SAML } from '@node-saml/node-saml'
import cookieParser from 'cookie-parser'
import express from 'express'
import passport from 'passport'
Expand All @@ -15,14 +16,14 @@ import { createProxy } from '../shared/proxy-utils.js'
import { RedisClient } from '../shared/redis-client.js'
import createSamlRouter from '../shared/routes/saml.js'
import { createSamlConfig } from '../shared/saml/index.js'
import redisCacheProvider from '../shared/saml/passport-saml-cache-redis.js'
import redisCacheProvider from '../shared/saml/node-saml-cache-redis.js'
import { sessionSupport } from '../shared/session.js'

import { createDevSfiRouter } from './dev-sfi-auth.js'
import { createKeycloakCitizenSamlStrategy } from './keycloak-citizen-saml.js'
import { authenticateKeycloakCitizen } from './keycloak-citizen-saml.js'
import mapRoutes from './mapRoutes.js'
import authStatus from './routes/auth-status.js'
import { createSuomiFiStrategy } from './suomi-fi-saml.js'
import { authenticateSuomiFi } from './suomi-fi-saml.js'

export function enduserGwRouter(
config: Config,
Expand Down Expand Up @@ -53,36 +54,37 @@ export function enduserGwRouter(
if (config.sfi.type === 'mock') {
router.use('/auth/saml', createDevSfiRouter(sessions))
} else if (config.sfi.type === 'saml') {
const suomifiSamlConfig = createSamlConfig(
config.sfi.saml,
redisCacheProvider(redisClient, { keyPrefix: 'suomifi-saml-resp:' })
)
router.use(
'/auth/saml',
createSamlRouter({
sessions,
strategyName: 'suomifi',
strategy: createSuomiFiStrategy(sessions, suomifiSamlConfig),
saml: new SAML(
createSamlConfig(
config.sfi.saml,
redisCacheProvider(redisClient, { keyPrefix: 'suomifi-saml-resp:' })
)
),
authenticate: authenticateSuomiFi,
defaultPageUrl: '/'
})
)
}

if (!config.keycloakCitizen)
throw new Error('Missing Keycloak SAML configuration (citizen)')
const keycloakCitizenConfig = createSamlConfig(
config.keycloakCitizen,
redisCacheProvider(redisClient, { keyPrefix: 'customer-saml-resp:' })
)
router.use(
'/auth/evaka-customer',
createSamlRouter({
sessions,
strategyName: 'evaka-customer',
strategy: createKeycloakCitizenSamlStrategy(
sessions,
keycloakCitizenConfig
saml: new SAML(
createSamlConfig(
config.keycloakCitizen,
redisCacheProvider(redisClient, { keyPrefix: 'customer-saml-resp:' })
)
),
authenticate: authenticateKeycloakCitizen,
defaultPageUrl: '/'
})
)
Expand Down
1 change: 0 additions & 1 deletion apigw/src/enduser/dev-sfi-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export function createDevSfiRouter(sessions: Sessions): Router {
return createDevAuthRouter({
sessions,
root: '/',
strategyName: 'dev-sfi',
loginFormHandler: async (req, res) => {
const defaultSsn = '070644-937X'

Expand Down
16 changes: 6 additions & 10 deletions apigw/src/enduser/keycloak-citizen-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SamlConfig, Strategy as SamlStrategy } from '@node-saml/passport-saml'
import { z } from 'zod'

import { createSamlStrategy } from '../shared/saml/index.js'
import { authenticateProfile } from '../shared/saml/index.js'
import { citizenLogin } from '../shared/service-client.js'
import { Sessions } from '../shared/session.js'

const Profile = z.object({
socialSecurityNumber: z.string(),
Expand All @@ -16,11 +14,9 @@ const Profile = z.object({
email: z.string()
})

export function createKeycloakCitizenSamlStrategy(
sessions: Sessions,
config: SamlConfig
): SamlStrategy {
return createSamlStrategy(sessions, config, Profile, async (profile) => {
export const authenticateKeycloakCitizen = authenticateProfile(
Profile,
async (profile) => {
const socialSecurityNumber = profile.socialSecurityNumber
if (!socialSecurityNumber)
throw Error('No socialSecurityNumber in evaka IDP SAML data')
Expand All @@ -38,5 +34,5 @@ export function createKeycloakCitizenSamlStrategy(
globalRoles: [],
allScopedRoles: []
}
})
}
}
)
16 changes: 6 additions & 10 deletions apigw/src/enduser/suomi-fi-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SamlConfig, Strategy } from '@node-saml/passport-saml'
import { z } from 'zod'

import { logWarn } from '../shared/logging.js'
import { createSamlStrategy } from '../shared/saml/index.js'
import { authenticateProfile } from '../shared/saml/index.js'
import { citizenLogin } from '../shared/service-client.js'
import { Sessions } from '../shared/session.js'

// Suomi.fi e-Identification – Attributes transmitted on an identified user:
// https://esuomi.fi/suomi-fi-services/suomi-fi-e-identification/14247-2/?lang=en
Expand All @@ -25,11 +23,9 @@ const Profile = z.object({

const ssnRegex = /^[0-9]{6}[-+ABCDEFUVWXY][0-9]{3}[0-9ABCDEFHJKLMNPRSTUVWXY]$/

export function createSuomiFiStrategy(
sessions: Sessions,
config: SamlConfig
): Strategy {
return createSamlStrategy(sessions, config, Profile, async (profile) => {
export const authenticateSuomiFi = authenticateProfile(
Profile,
async (profile) => {
const socialSecurityNumber = profile[SUOMI_FI_SSN_KEY]?.trim()
if (!socialSecurityNumber) throw Error('No SSN in SAML data')
if (!ssnRegex.test(socialSecurityNumber)) {
Expand All @@ -46,5 +42,5 @@ export function createSuomiFiStrategy(
globalRoles: [],
allScopedRoles: []
}
})
}
}
)
33 changes: 15 additions & 18 deletions apigw/src/internal/ad-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SamlConfig, Strategy as SamlStrategy } from '@node-saml/passport-saml'
import { z } from 'zod'

import { Config } from '../shared/config.js'
import { createSamlStrategy } from '../shared/saml/index.js'
import {
authenticateProfile,
AuthenticateProfile
} from '../shared/saml/index.js'
import { employeeLogin } from '../shared/service-client.js'
import { Sessions } from '../shared/session.js'

const AD_GIVEN_NAME_KEY =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'
Expand All @@ -19,20 +20,17 @@ const AD_EMAIL_KEY =
const AD_EMPLOYEE_NUMBER_KEY =
'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/employeenumber'

export function createAdSamlStrategy(
sessions: Sessions,
config: Config['ad'],
samlConfig: SamlConfig
): SamlStrategy {
const Profile = z
.object({
[AD_GIVEN_NAME_KEY]: z.string(),
[AD_FAMILY_NAME_KEY]: z.string(),
[AD_EMAIL_KEY]: z.string().optional(),
[AD_EMPLOYEE_NUMBER_KEY]: z.string().toLowerCase().optional()
})
.passthrough()
return createSamlStrategy(sessions, samlConfig, Profile, async (profile) => {
const Profile = z
.object({
[AD_GIVEN_NAME_KEY]: z.string(),
[AD_FAMILY_NAME_KEY]: z.string(),
[AD_EMAIL_KEY]: z.string().optional(),
[AD_EMPLOYEE_NUMBER_KEY]: z.string().toLowerCase().optional()
})
.passthrough()

export const authenticateAd = (config: Config['ad']): AuthenticateProfile =>
authenticateProfile(Profile, async (profile) => {
const aad = profile[config.userIdKey]
if (!aad || typeof aad !== 'string') throw Error('No user ID in SAML data')
const person = await employeeLogin({
Expand All @@ -49,4 +47,3 @@ export function createAdSamlStrategy(
allScopedRoles: person.allScopedRoles
}
})
}
25 changes: 12 additions & 13 deletions apigw/src/internal/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SAML } from '@node-saml/node-saml'
import cookieParser from 'cookie-parser'
import express from 'express'
import expressBasicAuth from 'express-basic-auth'
Expand All @@ -21,12 +22,12 @@ import { createProxy } from '../shared/proxy-utils.js'
import { RedisClient } from '../shared/redis-client.js'
import createSamlRouter from '../shared/routes/saml.js'
import { createSamlConfig } from '../shared/saml/index.js'
import redisCacheProvider from '../shared/saml/passport-saml-cache-redis.js'
import redisCacheProvider from '../shared/saml/node-saml-cache-redis.js'
import { sessionSupport } from '../shared/session.js'

import { createAdSamlStrategy } from './ad-saml.js'
import { authenticateAd } from './ad-saml.js'
import { createDevAdRouter } from './dev-ad-auth.js'
import { createKeycloakEmployeeSamlStrategy } from './keycloak-employee-saml.js'
import { authenticateKeycloakEmployee } from './keycloak-employee-saml.js'
import mobileDeviceSession, {
checkMobileEmployeeIdToken,
devApiE2ESignup,
Expand Down Expand Up @@ -81,34 +82,32 @@ export function internalGwRouter(
createSamlRouter({
sessions,
strategyName: 'ead',
strategy: createAdSamlStrategy(
sessions,
config.ad,
saml: new SAML(
createSamlConfig(
config.ad.saml,
redisCacheProvider(redisClient, { keyPrefix: 'ad-saml-resp:' })
)
),
authenticate: authenticateAd(config.ad),
defaultPageUrl: '/employee'
})
)
}

if (!config.keycloakEmployee)
throw new Error('Missing Keycloak SAML configuration (employee)')
const keycloakEmployeeConfig = createSamlConfig(
config.keycloakEmployee,
redisCacheProvider(redisClient, { keyPrefix: 'keycloak-saml-resp:' })
)
router.use(
'/auth/evaka',
createSamlRouter({
sessions,
strategyName: 'evaka',
strategy: createKeycloakEmployeeSamlStrategy(
sessions,
keycloakEmployeeConfig
saml: new SAML(
createSamlConfig(
config.keycloakEmployee,
redisCacheProvider(redisClient, { keyPrefix: 'keycloak-saml-resp:' })
)
),
authenticate: authenticateKeycloakEmployee,
defaultPageUrl: '/employee'
})
)
Expand Down
1 change: 0 additions & 1 deletion apigw/src/internal/dev-ad-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export function createDevAdRouter(sessions: Sessions): Router {
return createDevAuthRouter({
sessions,
root: '/employee',
strategyName: 'dev-ad',
loginFormHandler: async (req, res) => {
const employees = _.sortBy(await getEmployees(), ({ id }) => id)
const employeeInputs = employees
Expand Down
16 changes: 6 additions & 10 deletions apigw/src/internal/keycloak-employee-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { SamlConfig, Strategy as SamlStrategy } from '@node-saml/passport-saml'
import { z } from 'zod'

import { createSamlStrategy } from '../shared/saml/index.js'
import { authenticateProfile } from '../shared/saml/index.js'
import { employeeLogin } from '../shared/service-client.js'
import { Sessions } from '../shared/session.js'

const Profile = z.object({
id: z.string(),
Expand All @@ -16,11 +14,9 @@ const Profile = z.object({
lastName: z.string()
})

export function createKeycloakEmployeeSamlStrategy(
sessions: Sessions,
config: SamlConfig
): SamlStrategy {
return createSamlStrategy(sessions, config, Profile, async (profile) => {
export const authenticateKeycloakEmployee = authenticateProfile(
Profile,
async (profile) => {
const id = profile.id
if (!id) throw Error('No user ID in evaka IDP SAML data')
const person = await employeeLogin({
Expand All @@ -35,5 +31,5 @@ export function createKeycloakEmployeeSamlStrategy(
globalRoles: person.globalRoles,
allScopedRoles: person.allScopedRoles
}
})
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import { describe, beforeEach, expect, it, test } from '@jest/globals'
import type { CacheProvider } from '@node-saml/passport-saml'
import type { CacheProvider } from '@node-saml/node-saml'

import redisCacheProvider from '../saml/passport-saml-cache-redis.js'
import redisCacheProvider from '../saml/node-saml-cache-redis.js'
import { MockRedisClient } from '../test/mock-redis-client.js'

const ttlSeconds = 1
Expand All @@ -20,7 +20,7 @@ beforeEach(() => {
})
})

describe('passport-saml-cache-redis', () => {
describe('node-saml-cache-redis', () => {
describe('constructor', () => {
test('throws an error if ttlSeconds is not a positive integer', () => {
expect((): unknown =>
Expand Down
Loading

0 comments on commit 7a34576

Please sign in to comment.