Skip to content

Commit

Permalink
apigw: split SAML config creation from Strategies
Browse files Browse the repository at this point in the history
- Allows separate instation of the `SAML` class (from `passport-saml`) used for parsing/verifying SAML messages
- Will later be used to parse logout tokens from SAML LogoutRequest messages where cookies aren't available
  • Loading branch information
mikkopiu committed Apr 21, 2021
1 parent c512a9b commit 09b08da
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 66 deletions.
16 changes: 12 additions & 4 deletions apigw/src/internal/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ import mobileDeviceSession, {
} from './mobile-device-session'
import authStatus from './routes/auth-status'
import createSamlRouter from '../shared/routes/auth/saml'
import createAdSamlStrategy from '../shared/auth/ad-saml'
import createEvakaSamlStrategy from '../shared/auth/keycloak-saml'
import createAdSamlStrategy, {
createSamlConfig as createAdSamlConfig
} from '../shared/auth/ad-saml'
import createEvakaSamlStrategy, {
createSamlConfig as createEvakaSalmconfig
} from '../shared/auth/keycloak-saml'

const app = express()
trustReverseProxy(app)
Expand Down Expand Up @@ -69,19 +73,23 @@ function internalApiRouter() {
next()
})

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

const evakaSamlConfig = createEvakaSalmconfig()
router.use(
createSamlRouter({
strategyName: 'evaka',
strategy: createEvakaSamlStrategy(),
strategy: createEvakaSamlStrategy(evakaSamlConfig),
samlConfig: evakaSamlConfig,
sessionType: 'employee',
pathIdentifier: 'evaka'
})
Expand Down
43 changes: 25 additions & 18 deletions apigw/src/shared/auth/ad-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
Profile,
SamlConfig,
Strategy as SamlStrategy,
VerifiedCallback
} from 'passport-saml'
Expand Down Expand Up @@ -61,7 +62,29 @@ async function verifyProfile(profile: AdProfile): Promise<SamlUser> {
}
}

export default function createAdStrategy(): SamlStrategy | DevPassportStrategy {
export function createSamlConfig(): SamlConfig {
if (!adConfig) throw Error('Missing AD SAML configuration')
return {
callbackUrl: adConfig.callbackUrl,
entryPoint: adConfig.entryPointUrl,
logoutUrl: adConfig.logoutUrl,
issuer: adConfig.issuer,
cert: adConfig.publicCert.map(
(certificateName) => certificates[certificateName]
),
privateCert: readFileSync(adConfig.privateCert, {
encoding: 'utf8'
}),
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
disableRequestedAuthnContext: true,
signatureAlgorithm: 'sha256',
acceptedClockSkewMs: -1
}
}

export default function createAdStrategy(
config: SamlConfig
): SamlStrategy | DevPassportStrategy {
if (devLoginEnabled) {
const getter = async (userId: string) =>
verifyProfile({
Expand Down Expand Up @@ -98,24 +121,8 @@ export default function createAdStrategy(): SamlStrategy | DevPassportStrategy {

return new DevPassportStrategy(getter, upserter)
} else {
if (!adConfig) throw Error('Missing AD SAML configuration')
return new SamlStrategy(
{
callbackUrl: adConfig.callbackUrl,
entryPoint: adConfig.entryPointUrl,
logoutUrl: adConfig.logoutUrl,
issuer: adConfig.issuer,
cert: adConfig.publicCert.map(
(certificateName) => certificates[certificateName]
),
privateCert: readFileSync(adConfig.privateCert, {
encoding: 'utf8'
}),
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
disableRequestedAuthnContext: true,
signatureAlgorithm: 'sha256',
acceptedClockSkewMs: -1
},
config,
(profile: Profile, done: VerifiedCallback) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
verifyProfile((profile as any) as AdProfile)
Expand Down
33 changes: 20 additions & 13 deletions apigw/src/shared/auth/keycloak-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {
Profile,
SamlConfig,
Strategy as SamlStrategy,
VerifiedCallback
} from 'passport-saml'
Expand All @@ -12,27 +13,33 @@ import { getOrCreateEmployee } from '../service-client'
import { evakaSamlConfig } from '../config'
import fs from 'fs'

export default function createKeycloakSamlStrategy(): SamlStrategy {
export function createSamlConfig(): SamlConfig {
if (!evakaSamlConfig) throw new Error('Missing Keycloak SAML configuration')
const publicCert = fs.readFileSync(evakaSamlConfig.publicCert, {
encoding: 'utf8'
})
const privateCert = fs.readFileSync(evakaSamlConfig.privateCert, {
encoding: 'utf8'
})
return {
issuer: 'evaka',
callbackUrl: evakaSamlConfig.callbackUrl,
entryPoint: evakaSamlConfig.entryPoint,
logoutUrl: evakaSamlConfig.entryPoint,
acceptedClockSkewMs: -1,
cert: publicCert,
privateCert: privateCert,
decryptionPvk: privateCert,
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
signatureAlgorithm: 'sha256'
}
}

export default function createKeycloakSamlStrategy(
config: SamlConfig
): SamlStrategy {
return new SamlStrategy(
{
issuer: 'evaka',
callbackUrl: evakaSamlConfig.callbackUrl,
entryPoint: evakaSamlConfig.entryPoint,
logoutUrl: evakaSamlConfig.entryPoint,
acceptedClockSkewMs: -1,
cert: publicCert,
privateCert: privateCert,
decryptionPvk: privateCert,
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
signatureAlgorithm: 'sha256'
},
config,
(profile: Profile, done: VerifiedCallback) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
verifyKeycloakProfile(profile as KeycloakProfile)
Expand Down
62 changes: 34 additions & 28 deletions apigw/src/shared/auth/suomi-fi-saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { Profile, Strategy, VerifiedCallback } from 'passport-saml'
import { Profile, SamlConfig, Strategy, VerifiedCallback } from 'passport-saml'
import { SamlUser } from '../routes/auth/saml/types'
import { Strategy as DummyStrategy } from 'passport-dummy'
import { sfiConfig, sfiMock } from '../config'
Expand Down Expand Up @@ -52,40 +52,46 @@ async function verifyProfile(profile: SuomiFiProfile): Promise<SamlUser> {
sessionIndex: profile.sessionIndex
}
}
export default function createSuomiFiStrategy(): Strategy | DummyStrategy {

export function createSamlConfig(): SamlConfig {
if (sfiMock) return {}
if (!sfiConfig) throw new Error('Missing Suomi.fi SAML configuration')

const privateCert = fs.readFileSync(sfiConfig.privateCert, {
encoding: 'utf8'
})
return {
callbackUrl: sfiConfig.callbackUrl,
entryPoint: sfiConfig.entryPoint,
logoutUrl: sfiConfig.logoutUrl,
issuer: sfiConfig.issuer,
cert: sfiConfig.publicCert.map(
(certificateName) => certificates[certificateName]
),
privateCert: privateCert,
decryptionPvk: privateCert,
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
disableRequestedAuthnContext: true,
signatureAlgorithm: 'sha256',
acceptedClockSkewMs: -1
}
}

export default function createSuomiFiStrategy(
config: SamlConfig
): Strategy | DummyStrategy {
if (sfiMock) {
return new DummyStrategy((done) => {
verifyProfile(dummySuomiFiProfile)
.then((user) => done(null, user))
.catch(done)
})
} else {
if (!sfiConfig) throw new Error('Missing Suomi.fi SAML configuration')
const privateCert = fs.readFileSync(sfiConfig.privateCert, {
encoding: 'utf8'
return new Strategy(config, (profile: Profile, done: VerifiedCallback) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
verifyProfile((profile as any) as SuomiFiProfile)
.then((user) => done(null, user))
.catch(done)
})
return new Strategy(
{
callbackUrl: sfiConfig.callbackUrl,
entryPoint: sfiConfig.entryPoint,
logoutUrl: sfiConfig.logoutUrl,
issuer: sfiConfig.issuer,
cert: sfiConfig.publicCert.map(
(certificateName) => certificates[certificateName]
),
privateCert: privateCert,
decryptionPvk: privateCert,
identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
disableRequestedAuthnContext: true,
signatureAlgorithm: 'sha256',
acceptedClockSkewMs: -1
},
(profile: Profile, done: VerifiedCallback) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
verifyProfile((profile as any) as SuomiFiProfile)
.then((user) => done(null, user))
.catch(done)
}
)
}
}
3 changes: 2 additions & 1 deletion apigw/src/shared/routes/auth/saml/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import { Strategy } from 'passport-saml'
import { SamlConfig, Strategy } from 'passport-saml'
import { Strategy as DummyStrategy } from 'passport-dummy'
import { SessionType } from '../../../session'
import { UserType } from '../../../service-client'

export interface SamlEndpointConfig {
strategyName: string
strategy: Strategy | DummyStrategy
samlConfig: SamlConfig
sessionType: SessionType
pathIdentifier: string
}
Expand Down
8 changes: 6 additions & 2 deletions apigw/src/shared/routes/auth/suomi-fi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import createSamlRouter from './saml'
import createSuomiFiStrategy from '../../auth/suomi-fi-saml'
import createSuomiFiStrategy, {
createSamlConfig as createSuomiFiSamlConfig
} from '../../auth/suomi-fi-saml'
import { SessionType } from '../../session'

export function createAuthEndpoints(sessionType: SessionType) {
const samlConfig = createSuomiFiSamlConfig()
return createSamlRouter({
strategyName: 'suomifi',
strategy: createSuomiFiStrategy(),
strategy: createSuomiFiStrategy(samlConfig),
samlConfig,
sessionType,
pathIdentifier: 'saml'
})
Expand Down

0 comments on commit 09b08da

Please sign in to comment.