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

[EVAKA-HOTFIX] Support Single Logout w/o 3rd party cookies #738

Merged
merged 20 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8b782ad
APIGW: don't use deprecated body-parser directly
mikkopiu Apr 14, 2021
4907f8e
APIGW: split SAML config creation from Strategies
mikkopiu Apr 15, 2021
a49d0e8
APIGW: clarify logout start error logging
mikkopiu Apr 19, 2021
3203430
APIGW: support SLO without cookies, drop logout cookie
mikkopiu Apr 20, 2021
3f9ec9f
APIGW: Make SAML parser optional for Express logout
mikkopiu Apr 27, 2021
11b8190
Use a more recognizable separator for SLO token key
mikkopiu Apr 28, 2021
d8bfa3a
APIGW: fall back to logoutToken in logout
mikkopiu Apr 29, 2021
541544d
APIGW: Refactor SAML-specifics out of session module
mikkopiu Apr 29, 2021
2d5c062
APIGW: Tighten ESLint config for Promises
mikkopiu May 10, 2021
c77891d
Allow manually setting cookies in GatewayTester
mikkopiu May 10, 2021
a79a2d8
APIGW: add .editorconfig
mikkopiu May 10, 2021
37281c5
APIGW: Add test cert and key for SLO testing
mikkopiu May 10, 2021
8b6d034
APIGW: Allow overriding mock config for Suomi.fi Strategy for testing
mikkopiu May 10, 2021
bd9e408
GatewayTester: allow overriding login POST data
mikkopiu May 10, 2021
9a174ea
AsyncRedisClient: support array of keys for del, workaround for redis…
mikkopiu May 10, 2021
dc56a6c
APIGW: Regression tests for SAML Single Logout
mikkopiu May 10, 2021
b9d8e0b
Fix typo
mikkopiu May 11, 2021
784c1f3
APIGW: SAML parsing cleanup
mikkopiu May 11, 2021
68206de
APIGW: Improve test failure stack traces for requests
mikkopiu May 11, 2021
6a62fcd
APIGW: Use real app instance for SAML tests, mock Redis in tests
mikkopiu May 11, 2021
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
13 changes: 13 additions & 0 deletions apigw/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2017-2021 City of Espoo
#
# SPDX-License-Identifier: LGPL-2.1-or-later

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
22 changes: 22 additions & 0 deletions apigw/config/test-cert/slo-test-idp-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDpzCCAo+gAwIBAgIUXeLWbrFuIwl9965EUMbgD/bwRXYwDQYJKoZIhvcNAQEL
BQAwYjELMAkGA1UEBhMCRkkxEDAOBgNVBAgMB1V1c2ltYWExDjAMBgNVBAcMBUVz
cG9vMRgwFgYDVQQKDA9Fc3Bvb24ga2F1cHVua2kxFzAVBgNVBAMMDmV2YWthLXNs
by10ZXN0MCAXDTIxMDUwNzA5NTAwOVoYDzIxMjEwNDEzMDk1MDA5WjBiMQswCQYD
VQQGEwJGSTEQMA4GA1UECAwHVXVzaW1hYTEOMAwGA1UEBwwFRXNwb28xGDAWBgNV
BAoMD0VzcG9vbiBrYXVwdW5raTEXMBUGA1UEAwwOZXZha2Etc2xvLXRlc3QwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8065qJdUM3YGJTUVjIXJB4CBt
/qSr28OIPAb2nOT++3nS8C4jsx+nuArx2kGlyF3DlyEQRlW44fMtnhO3uSvHQ33s
rsflw5kb0XysePoXLd8uxo2993aZGFzw/fK07PB52WuHFm+GL5EIptwQDe7M/qe9
IbwtmFkFdMY67LkG4YwW5j7MHKD7jLPQFMqGCYG9jfIwtuPK2aEoTP1QYOrL4L6E
8bufk628u8lNQJZ6F5VvzRKO/NPIp5oC8A1vVh4OJJa2b98NY34ehv3siuqwtFcu
MTS8Ul6uEJ8+705k1c6OV6n1e/4d2NwkJWqjrRv2fLlsxSUucfQp3HGrckCFAgMB
AAGjUzBRMB0GA1UdDgQWBBSnWKQiasD8qm/VrhGA75gBeodcETAfBgNVHSMEGDAW
gBSnWKQiasD8qm/VrhGA75gBeodcETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQBi2O3gfVeIIPcPMQawK2oqKth4/KireobEXiHwWrheomS6CEQG
rdzxt8r4aq25Y/Lj3vDZKo9aWxC0fM1heYxaaXn4F7GOJna/AJPMi4ccxv/OLtOA
Y9gUM58i5sLvUg+Z8UrN/HJTFpFx2yJsSE7UzAS+9Zh/iF/MCHmjTKJ4BkwFcTr3
SG7Zo5nT+BrQlhxHSYGseouHyh8JfIjRVnLJ1C6LDuNUlTn7XY3ExtRm/hnqlUlc
XZ5WbtGWzCOodFGbRbVA6sommMeCtBoKp4o74GbqxpzkJel9q7CD0TVscmt8cvKV
jdw6/QQLeq8SQYo3UI/90HZPBW/3+UVrtcNi
-----END CERTIFICATE-----
28 changes: 28 additions & 0 deletions apigw/config/test-cert/slo-test-idp-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8065qJdUM3YGJ
TUVjIXJB4CBt/qSr28OIPAb2nOT++3nS8C4jsx+nuArx2kGlyF3DlyEQRlW44fMt
nhO3uSvHQ33srsflw5kb0XysePoXLd8uxo2993aZGFzw/fK07PB52WuHFm+GL5EI
ptwQDe7M/qe9IbwtmFkFdMY67LkG4YwW5j7MHKD7jLPQFMqGCYG9jfIwtuPK2aEo
TP1QYOrL4L6E8bufk628u8lNQJZ6F5VvzRKO/NPIp5oC8A1vVh4OJJa2b98NY34e
hv3siuqwtFcuMTS8Ul6uEJ8+705k1c6OV6n1e/4d2NwkJWqjrRv2fLlsxSUucfQp
3HGrckCFAgMBAAECggEAPfgqkWOBHAvF602UrAfZ+4yWmAKuAEjLTvaEQoMTFCtr
u7JfMhAjH2PjE6RRTxsGyp3amAC9OUPODvaF+hGnMGoR9Y8Ww20B3oNNqzy4tsqz
KCK5edKw9WVtexmcgYwRD6wvAdJ3H06VBoXcStiHuncIjaV4oG4TKRs9wzDVOFBT
lN/3JPD8Q6E0hdpd9t+27s7caNOjhVo1x1xyLfX2sxC81BeXA6sUo3nmthWJyspr
l3ReMJsNSe7tpZQojxNh+4xQrVuuCRDt0ic2HLczFiHG0f5iHp4tXmRHRmfH2co+
MwEfnFAGnUu4zUkb8y5asAYuW6IIzIpvfpgw5URTMQKBgQD7NiZS1zOlJLsnolP5
tGBt/zgGeyNYKt53EfCle1i2mwwZLTK/+WPdtf+E/36AbcSnwkjq5IVW3SGWYIo0
Awg9i5mie4QtT9wYAiz/gLfZ1tVlUbOvo3TY34h5Ata1IK23xXeuA0/NMPzDyRRy
UOECgmzZb3Ko4Aqe5beUP3tcrwKBgQDAbRwkdY0hHE/bCEwT4xDYiLSIEJtBDjnI
Os27c56xG447ThOAcVy0kolMQTvTrPVMZlbn20zOgxycOBAT8fJbjCH4ivL8Yl4i
UyN6HQOVPTkbbX8jJqsy6SBW3EScfCs7PT4H4QmLLa1RJIelw47yGVqwmIbaBSQD
B/ftTmJLCwKBgCQQ3SWtkdOW12vUSVwjQmjoaGG90hA5b2EG6VbIw67Lycvfila3
dlgBZiLxD3deywoOwas/jckvzD+rsovPF6LGZRNHym069u1XeqBgGYUj69U1Cqgf
vonYZd6BwtOUUnx81DbecNmTu+Zb+xyCchuLIBeDgaGvMLcpYdbd2lcvAoGBAIeV
2fiOo6yq6FGrXP++RQZt/NbK7LpALdK6LHBinXSpt+RttSwRtIK/peKHLIKQIh99
FMs2KL5yf9xLXHjRSDXdXaplLaVMIowJDLxkaTvk8bIzyxuXiZXL0i+h8O5aR5Ps
KSMgG7tnqfG8zZ+tVbGcz9wS/SHt8Vv5Z2ZcjsHVAoGBAIRniS4Opbh3EVcRMhsE
tFGzaErUt6ml3IIL0NljtipjMNWKzs9SduFgBMCdSjj5YH85vAq/sDyIr3imd2JB
e8icpm1kNue3BAuGjmpQFpHQR5AMchmjTezZYp8b9VdiEFHNPIhxrNL6WecFpesE
d/dGyI8q01wFiAmPn0fIPpa5
-----END PRIVATE KEY-----
11 changes: 9 additions & 2 deletions apigw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"@types/through2": "^2.0.34",
"@types/tough-cookie": "^4.0.0",
"@types/uuid": "^8.3.0",
"@types/xml-crypto": "^1.4.1",
"@types/xml2js": "^0.4.8",
"@types/xmldom": "^0.1.30",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"concurrently": "^5.3.0",
Expand All @@ -86,7 +89,10 @@
"tough-cookie": "^4.0.0",
"ts-jest": "^26.4.3",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"xml-crypto": "2.1.2",
"xml2js": "^0.4.23",
"xmldom": "^0.6.0"
},
"resolutions": {
"@types/node": "^14.14.6",
Expand Down Expand Up @@ -152,7 +158,8 @@
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
],
"@typescript-eslint/no-floating-promises": "error"
}
},
"engines": {
Expand Down
11 changes: 11 additions & 0 deletions apigw/src/__mocks__/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2017-2021 City of Espoo
//
// SPDX-License-Identifier: LGPL-2.1-or-later

// Automatic mock for Jest that replaces all imports of redis with redis-mock.
// See: https://jestjs.io/docs/manual-mocks#mocking-node-modules for details

import redis from 'redis-mock'

export const createClient = redis.createClient
export default redis
28 changes: 17 additions & 11 deletions apigw/src/enduser/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import express, { Router } from 'express'
import helmet from 'helmet'
import nocache from 'nocache'
import passport from 'passport'
import { requireAuthentication } from '../shared/auth'
import createEvakaCustomerSamlStrategy from '../shared/auth/customer-saml'
import createSuomiFiStrategy from '../shared/auth/suomi-fi-saml'
import { nodeEnv } from '../shared/config'
import createEvakaCustomerSamlStrategy, {
createSamlConfig as createEvakaCustomerSamlConfig
} from '../shared/auth/customer-saml'
import createSuomiFiStrategy, {
createSamlConfig as createSuomiFiSamlConfig
} from '../shared/auth/suomi-fi-saml'
import setupLoggingMiddleware from '../shared/logging'
import { csrf, csrfCookie } from '../shared/middleware/csrf'
import { errorHandler } from '../shared/middleware/error-handler'
Expand All @@ -25,8 +27,7 @@ import routes from './routes'
import authStatus from './routes/auth-status'

const app = express()
// TODO: How to make this more easily injectable/overridable in tests?
const redisClient = nodeEnv !== 'test' ? createRedisClient() : undefined
const redisClient = createRedisClient()
trustReverseProxy(app)
app.set('etag', false)
app.use(nocache())
Expand All @@ -36,34 +37,38 @@ app.use(
contentSecurityPolicy: false
})
)
app.get('/health', (req, res) => res.status(200).json({ status: 'UP' }))
app.get('/health', (_, res) => res.status(200).json({ status: 'UP' }))
app.use(tracing)
app.use(bodyParser.json())
app.use(express.json())
app.use(cookieParser())
app.use(session('enduser', redisClient))
app.use(passport.initialize())
app.use(passport.session())
passport.serializeUser<Express.User>((user, done) => done(null, user))
passport.deserializeUser<Express.User>((user, done) => done(null, user))
app.use(refreshLogoutToken('enduser'))
app.use(refreshLogoutToken())
setupLoggingMiddleware(app)

function apiRouter() {
const router = Router()

router.use(publicRoutes)
const suomifiSamlConfig = createSuomiFiSamlConfig(redisClient)
router.use(
createSamlRouter({
strategyName: 'suomifi',
strategy: createSuomiFiStrategy(redisClient),
strategy: createSuomiFiStrategy(suomifiSamlConfig),
samlConfig: suomifiSamlConfig,
sessionType: 'enduser',
pathIdentifier: 'saml'
})
)
const evakaCustomerSamlConfig = createEvakaCustomerSamlConfig(redisClient)
router.use(
createSamlRouter({
strategyName: 'evaka-customer',
strategy: createEvakaCustomerSamlStrategy(redisClient),
strategy: createEvakaCustomerSamlStrategy(evakaCustomerSamlConfig),
samlConfig: evakaCustomerSamlConfig,
sessionType: 'enduser',
pathIdentifier: 'evaka-customer'
})
Expand All @@ -79,3 +84,4 @@ app.use('/api/application', apiRouter())
app.use(errorHandler(false))

export default app
export const _TEST_ONLY_redisClient = redisClient
34 changes: 20 additions & 14 deletions apigw/src/internal/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import express, { Router } from 'express'
import helmet from 'helmet'
import nocache from 'nocache'
import passport from 'passport'
import { requireAuthentication } from '../shared/auth'
import createAdSamlStrategy from '../shared/auth/ad-saml'
import createEvakaSamlStrategy from '../shared/auth/keycloak-saml'
import { cookieSecret, enableDevApi, nodeEnv } from '../shared/config'
import createAdSamlStrategy, {
createSamlConfig as createAdSamlConfig
} from '../shared/auth/ad-saml'
import createEvakaSamlStrategy, {
createSamlConfig as createEvakaSamlconfig
} from '../shared/auth/keycloak-saml'
import { cookieSecret, enableDevApi } from '../shared/config'
import setupLoggingMiddleware from '../shared/logging'
import { csrf, csrfCookie } from '../shared/middleware/csrf'
import { errorHandler } from '../shared/middleware/error-handler'
Expand All @@ -29,8 +32,7 @@ import mobileDeviceSession, {
import authStatus from './routes/auth-status'

const app = express()
// TODO: How to make this more easily injectable/overridable in tests?
const redisClient = nodeEnv !== 'test' ? createRedisClient() : undefined
const redisClient = createRedisClient()
trustReverseProxy(app)
app.set('etag', false)
app.use(nocache())
Expand All @@ -40,51 +42,55 @@ app.use(
contentSecurityPolicy: false
})
)
app.get('/health', (req, res) => res.status(200).json({ status: 'UP' }))
app.get('/health', (_, res) => res.status(200).json({ status: 'UP' }))
app.use(tracing)
app.use(bodyParser.json({ limit: '8mb' }))
app.use(express.json({ limit: '8mb' }))
app.use(session('employee', redisClient))
app.use(cookieParser(cookieSecret))
app.use(passport.initialize())
app.use(passport.session())
passport.serializeUser<Express.User>((user, done) => done(null, user))
passport.deserializeUser<Express.User>((user, done) => done(null, user))
app.use(refreshLogoutToken('employee'))
app.use(refreshLogoutToken())
setupLoggingMiddleware(app)

app.use('/api/csp', csp)

function scheduledApiRouter() {
const router = Router()
router.all('*', (req, res) => res.sendStatus(404))
router.all('*', (_, res) => res.sendStatus(404))
return router
}

function internalApiRouter() {
const router = Router()
router.use('/scheduled', scheduledApiRouter())
router.all('/system/*', (req, res) => res.sendStatus(404))
router.all('/system/*', (_, res) => res.sendStatus(404))

router.all('/auth/*', (req: express.Request, res, next) => {
if (req.session?.logoutToken?.idpProvider === 'evaka') {
if (req.session?.idpProvider === 'evaka') {
req.url = req.url.replace('saml', 'evaka')
}
next()
})

const adSamlConfig = createAdSamlConfig(redisClient)
router.use(
createSamlRouter({
strategyName: 'ead',
strategy: createAdSamlStrategy(redisClient),
strategy: createAdSamlStrategy(adSamlConfig),
samlConfig: adSamlConfig,
sessionType: 'employee',
pathIdentifier: 'saml'
})
)

const evakaSamlConfig = createEvakaSamlconfig(redisClient)
router.use(
createSamlRouter({
strategyName: 'evaka',
strategy: createEvakaSamlStrategy(redisClient),
strategy: createEvakaSamlStrategy(evakaSamlConfig),
samlConfig: evakaSamlConfig,
sessionType: 'employee',
pathIdentifier: 'evaka'
})
Expand Down
8 changes: 4 additions & 4 deletions apigw/src/shared/__tests__/saml-certificates-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import { differenceInMonths } from 'date-fns'
import certificates from '../certificates'
import certificates, { TrustedCertificates } from '../certificates'
import { pki } from 'node-forge'

describe('SAML certificates', () => {
test('at least one certificate must exist', () => {
expect(Object.keys(certificates).length).toBeGreaterThan(0)
})
for (const certificateName of Object.keys(certificates) as Array<
keyof typeof certificates
>) {
for (const certificateName of Object.keys(
certificates
) as Array<TrustedCertificates>) {
test(`${certificateName} must decode successfully`, () => {
const computeHash = false
const strict = true
Expand Down
Loading