1
1
// useful tutorial
2
2
// https://medium.com/disney-streaming/setup-a-single-sign-on-saml-test-environment-with-docker-and-nodejs-c53fc1a984c9
3
3
4
+ import type { Request } from 'express'
4
5
import { readFile , access , constants } from 'node:fs/promises'
5
- import type { Saml2 } from '../../config/type/index.ts'
6
+ import type { SAML2 } from '../../config/type/index.ts'
6
7
import config from '#config'
7
8
import _slug from 'slugify'
8
9
import samlify from 'samlify'
@@ -11,14 +12,16 @@ import mongo from '#mongo'
11
12
import { decipher , cipher } from '../utils/cipher.ts'
12
13
import { exec } from 'node:child_process'
13
14
import { promisify } from 'node:util'
15
+ import { getSiteBaseUrl , reqSite } from '#services'
16
+ import { type Site } from '#types'
14
17
15
18
const execAsync = promisify ( exec )
16
19
const debug = Debug ( 'saml' )
17
20
const slug = _slug . default
18
21
19
22
type Certificates = { signing : { privateKey : string , cert : string } , encrypt : { privateKey : string , cert : string } }
20
23
21
- type PreparedSaml2Provider = Saml2 & { id : string , idp : samlify . IdentityProviderInstance }
24
+ type PreparedSaml2Provider = SAML2 & { id : string , idp : samlify . IdentityProviderInstance }
22
25
23
26
// const validator = require('@authenio/samlify-xsd-schema-validator')
24
27
// samlify.setSchemaValidator(validator)
@@ -29,6 +32,27 @@ samlify.setSchemaValidator({
29
32
}
30
33
} )
31
34
35
+ export const getSamlProviderById = async ( req : Request , id : string ) : Promise < PreparedSaml2Provider | undefined > => {
36
+ const site = await reqSite ( req )
37
+ if ( ! site ) {
38
+ return saml2GlobalProviders ( ) . find ( p => p . id === id )
39
+ } else {
40
+ const providerInfo = site . authProviders ?. find ( p => p . type === 'saml2' && getSamlConfigId ( p ) === id ) as SAML2
41
+ const idp = samlify . IdentityProvider ( providerInfo )
42
+ return {
43
+ id,
44
+ ...providerInfo ,
45
+ idp
46
+ }
47
+ }
48
+ }
49
+
50
+ export const getSamlConfigId = ( providerConfig : SAML2 ) => {
51
+ const idp = samlify . IdentityProvider ( providerConfig )
52
+ if ( ! idp . entityMeta . meta . entityID ) throw new Error ( 'missing entityID in saml IDP metadata' )
53
+ return getSamlProviderId ( idp . entityMeta . meta . entityID )
54
+ }
55
+
32
56
export const getSamlProviderId = ( url : string ) => {
33
57
return slug ( new URL ( url ) . host , { lower : true , strict : true } )
34
58
}
@@ -56,8 +80,16 @@ const readDeprecatedCertificates = async (): Promise<undefined | Certificates> =
56
80
}
57
81
}
58
82
59
- const readCertificates = async ( ) : Promise < undefined | Certificates > => {
60
- const secret = await mongo . secrets . findOne ( { _id : 'saml-certificates' } )
83
+ const getSiteCertKey = ( site ?: Site ) => {
84
+ if ( ! site ) return 'saml-certificates'
85
+ let key = 'saml-certificates-' + slug ( site . host )
86
+ if ( site . path ) key += `--${ slug ( site . path ) } `
87
+ return key
88
+ }
89
+
90
+ const readCertificates = async ( site ?: Site ) : Promise < undefined | Certificates > => {
91
+ const key = getSiteCertKey ( site )
92
+ const secret = await mongo . secrets . findOne ( { _id : key } )
61
93
if ( secret ) {
62
94
const certificates = secret . data
63
95
certificates . signing . privateKey = decipher ( certificates . signing . privateKey )
@@ -66,52 +98,47 @@ const readCertificates = async (): Promise<undefined | Certificates> => {
66
98
}
67
99
}
68
100
69
- const writeCertificates = async ( certificates : Certificates ) => {
101
+ const writeCertificates = async ( certificates : Certificates , site ?: Site ) => {
102
+ const key = getSiteCertKey ( site )
70
103
const storedCertificates = { signing : { ...certificates . signing } , encrypt : { ...certificates . encrypt } } as any
71
104
storedCertificates . signing . privateKey = cipher ( certificates . signing . privateKey )
72
105
storedCertificates . encrypt . privateKey = cipher ( certificates . encrypt . privateKey )
73
- await mongo . secrets . insertOne ( { _id : 'saml-certificates' , data : storedCertificates } )
106
+ await mongo . secrets . insertOne ( { _id : key , data : storedCertificates } )
74
107
}
75
108
76
109
const _globalProviders : PreparedSaml2Provider [ ] = [ ]
77
110
let _sp : samlify . ServiceProviderInstance | undefined
78
- export const saml2ServiceProvider = ( ) => {
79
- if ( ! _sp ) throw new Error ( 'Global Saml 2 providers ware not initialized' )
111
+ export const saml2ServiceProvider = async ( site ?: Site ) => {
112
+ if ( ! _sp ) throw new Error ( 'Global Saml 2 provider was not initialized' )
113
+ if ( site ) return await initServiceProvider ( site )
80
114
return _sp
81
115
}
82
116
export const saml2GlobalProviders = ( ) => {
83
- if ( ! _sp ) throw new Error ( 'Global Saml 2 providers ware not initialized' )
117
+ if ( ! _sp ) throw new Error ( 'Global Saml 2 provider was not initialized' )
84
118
return _globalProviders
85
119
}
86
120
87
- export const init = async ( ) => {
88
- let certificates = await readCertificates ( )
121
+ const initCertificates = async ( site ?: Site ) => {
122
+ let certificates = await readCertificates ( site )
89
123
if ( ! certificates ) {
90
124
console . log ( 'Initializing SAML certificates' )
91
- certificates = await readDeprecatedCertificates ( )
92
- if ( certificates ) {
93
- console . log ( 'Migrating SAML certificates from filesystem to database' )
94
- } else {
125
+ if ( ! site ) {
126
+ certificates = await readDeprecatedCertificates ( )
127
+ if ( certificates ) {
128
+ console . log ( 'Migrating SAML certificates from filesystem to database' )
129
+ }
130
+ }
131
+ if ( ! certificates ) {
95
132
console . log ( 'Generating new SAML certificates' )
96
133
certificates = { signing : await createCert ( ) , encrypt : await createCert ( ) }
97
134
}
98
- await writeCertificates ( certificates )
135
+ await writeCertificates ( certificates , site )
99
136
}
137
+ return certificates
138
+ }
100
139
101
- const assertionConsumerService = [ {
102
- Binding : samlify . Constants . namespace . binding . post ,
103
- Location : `${ config . publicUrl } /api/auth/saml2-assert`
104
- } ]
105
- debug ( 'config service provider' )
106
- _sp = samlify . ServiceProvider ( {
107
- entityID : `${ config . publicUrl } /api/auth/saml2-metadata.xml` ,
108
- assertionConsumerService,
109
- signingCert : certificates . signing . cert ,
110
- privateKey : certificates . signing . privateKey ,
111
- encryptCert : certificates . encrypt . cert ,
112
- encPrivateKey : certificates . encrypt . privateKey ,
113
- ...config . saml2 . sp
114
- } )
140
+ export const init = async ( ) => {
141
+ _sp = await initServiceProvider ( )
115
142
116
143
debug ( 'config identity providers' )
117
144
for ( const providerConfig of config . saml2 . providers ) {
@@ -126,6 +153,27 @@ export const init = async () => {
126
153
}
127
154
}
128
155
156
+ export const initServiceProvider = async ( site ?: Site ) => {
157
+ const certificates = await initCertificates ( site )
158
+ const url = site ? `${ getSiteBaseUrl ( site ) } /simple-directory` : config . publicUrl
159
+ const assertionConsumerService = [ {
160
+ Binding : samlify . Constants . namespace . binding . post ,
161
+ Location : `${ url } /api/auth/saml2-assert`
162
+ } ]
163
+ debug ( 'config service provider' )
164
+ return samlify . ServiceProvider ( {
165
+ entityID : `${ url } /api/auth/saml2-metadata.xml` ,
166
+ assertionConsumerService,
167
+ signingCert : certificates . signing . cert ,
168
+ privateKey : certificates . signing . privateKey ,
169
+ encryptCert : certificates . encrypt . cert ,
170
+ encPrivateKey : certificates . encrypt . privateKey ,
171
+ // @ts -ignore if we use a boolean the attribute is set as empty in the xml output, and some IDP don't like that
172
+ allowCreate : 'false' ,
173
+ ...config . saml2 . sp
174
+ } )
175
+ }
176
+
129
177
const createCert = async ( ) => {
130
178
const subject = `/C=FR/CN=${ new URL ( config . publicUrl ) . hostname } `
131
179
const privateKey = ( await execAsync ( 'openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048' ) ) . stdout
0 commit comments