Skip to content

Commit

Permalink
Merge pull request #738 from espoon-voltti/fix/sfi-double-logout
Browse files Browse the repository at this point in the history
[EVAKA-HOTFIX] Support Single Logout w/o 3rd party cookies
  • Loading branch information
mikkopiu authored May 12, 2021
2 parents 8accab1 + 6a62fcd commit 6b872a7
Show file tree
Hide file tree
Showing 27 changed files with 927 additions and 259 deletions.
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

0 comments on commit 6b872a7

Please sign in to comment.