@sphereon/ssi-express-support 0.14.2-next.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +25 -0
  3. package/dist/auth-utils.d.ts +19 -0
  4. package/dist/auth-utils.d.ts.map +1 -0
  5. package/dist/auth-utils.js +118 -0
  6. package/dist/auth-utils.js.map +1 -0
  7. package/dist/entra-id-auth.d.ts +10 -0
  8. package/dist/entra-id-auth.d.ts.map +1 -0
  9. package/dist/entra-id-auth.js +61 -0
  10. package/dist/entra-id-auth.js.map +1 -0
  11. package/dist/express-builders.d.ts +94 -0
  12. package/dist/express-builders.d.ts.map +1 -0
  13. package/dist/express-builders.js +269 -0
  14. package/dist/express-builders.js.map +1 -0
  15. package/dist/express-utils.d.ts +4 -0
  16. package/dist/express-utils.d.ts.map +1 -0
  17. package/dist/express-utils.js +34 -0
  18. package/dist/express-utils.js.map +1 -0
  19. package/dist/functions.d.ts +2 -0
  20. package/dist/functions.d.ts.map +1 -0
  21. package/dist/functions.js +11 -0
  22. package/dist/functions.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/openid-connect-auth.d.ts +10 -0
  28. package/dist/openid-connect-auth.d.ts.map +1 -0
  29. package/dist/openid-connect-auth.js +61 -0
  30. package/dist/openid-connect-auth.js.map +1 -0
  31. package/dist/static-bearer-auth.d.ts +34 -0
  32. package/dist/static-bearer-auth.d.ts.map +1 -0
  33. package/dist/static-bearer-auth.js +146 -0
  34. package/dist/static-bearer-auth.js.map +1 -0
  35. package/dist/types.d.ts +179 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +8 -0
  38. package/dist/types.js.map +1 -0
  39. package/package.json +70 -0
  40. package/src/auth-utils.ts +127 -0
  41. package/src/entra-id-auth.ts +47 -0
  42. package/src/express-builders.ts +320 -0
  43. package/src/express-utils.ts +30 -0
  44. package/src/functions.ts +6 -0
  45. package/src/index.ts +7 -0
  46. package/src/openid-connect-auth.ts +47 -0
  47. package/src/static-bearer-auth.ts +151 -0
  48. package/src/types.ts +192 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @public
3
+ */
4
+ import bodyParser from 'body-parser'
5
+ import { Enforcer } from 'casbin'
6
+ import cors, { CorsOptions } from 'cors'
7
+ import * as dotenv from 'dotenv-flow'
8
+ import express, { Express } from 'express'
9
+ import { Application, ApplicationRequestHandler } from 'express-serve-static-core'
10
+ import session from 'express-session'
11
+ import http from 'http'
12
+ import morgan from 'morgan'
13
+ import passport, { InitializeOptions } from 'passport'
14
+ import { checkUserIsInRole } from './auth-utils'
15
+ import { jsonErrorHandler } from './express-utils'
16
+ import { env } from './functions'
17
+ import { ExpressSupport, IExpressServerOpts } from './types'
18
+
19
+ type Handler<Request extends http.IncomingMessage, Response extends http.ServerResponse> = (
20
+ req: Request,
21
+ res: Response,
22
+ callback: (err?: Error) => void
23
+ ) => void
24
+ dotenv.config()
25
+
26
+ export class ExpressBuilder {
27
+ private existingExpress?: Express
28
+ private hostnameOrIP?: string
29
+ private port?: number
30
+ private _handlers?: ApplicationRequestHandler<Application>[] = []
31
+ private listenCallback?: () => void
32
+ private _startListen?: boolean | undefined = undefined
33
+ private readonly envVarPrefix?: string
34
+ private _corsConfigurer?: ExpressCorsConfigurer
35
+ private _sessionOpts?: session.SessionOptions
36
+ private _usePassportAuth?: boolean = false
37
+ private _passportInitOpts?: InitializeOptions
38
+ private _userIsInRole?: string | string[]
39
+ private _enforcer?: Enforcer
40
+ private _morgan?: Handler<any, any> | undefined
41
+
42
+ private constructor(opts?: { existingExpress?: Express; envVarPrefix?: string }) {
43
+ const { existingExpress, envVarPrefix } = opts ?? {}
44
+ if (existingExpress) {
45
+ this.withExpress(existingExpress)
46
+ }
47
+ this.envVarPrefix = envVarPrefix ?? ''
48
+ }
49
+
50
+ public static fromExistingExpress(opts?: { existingExpress?: Express; envVarPrefix?: string }) {
51
+ return new ExpressBuilder(opts ?? {})
52
+ }
53
+
54
+ public static fromServerOpts(opts: IExpressServerOpts & { envVarPrefix?: string }) {
55
+ const builder = new ExpressBuilder({ existingExpress: opts?.existingExpress, envVarPrefix: opts?.envVarPrefix })
56
+ return builder.withEnableListenOpts(opts)
57
+ }
58
+
59
+ public enableListen(startOnBuild?: boolean): this {
60
+ if (startOnBuild !== undefined) {
61
+ this._startListen = startOnBuild
62
+ }
63
+ return this
64
+ }
65
+
66
+ public withMorganLogging(opts?: { existingMorgan?: Handler<any, any>; format?: string; options?: morgan.Options<any, any> }): this {
67
+ if (opts?.existingMorgan && (opts.format || opts.options)) {
68
+ throw Error('Cannot using an existing morgan with either a format or options')
69
+ }
70
+ this._morgan = opts?.existingMorgan ?? morgan(opts?.format ?? 'dev', opts?.options)
71
+ return this
72
+ }
73
+
74
+ public withEnableListenOpts({
75
+ port,
76
+ hostnameOrIP,
77
+ callback,
78
+ startOnBuild,
79
+ }: {
80
+ port?: number
81
+ hostnameOrIP?: string
82
+ startOnBuild?: boolean
83
+ callback?: () => void
84
+ }): this {
85
+ port && this.withPort(port)
86
+ hostnameOrIP && this.withHostname(hostnameOrIP)
87
+ if (typeof callback === 'function') {
88
+ this.withListenCallback(callback)
89
+ }
90
+ this._startListen = startOnBuild !== false
91
+ return this
92
+ }
93
+
94
+ public withPort(port: number): this {
95
+ this.port = port
96
+ return this
97
+ }
98
+
99
+ public withHostname(hostnameOrIP: string): this {
100
+ this.hostnameOrIP = hostnameOrIP
101
+ return this
102
+ }
103
+
104
+ public withListenCallback(callback: () => void): this {
105
+ this.listenCallback = callback
106
+ return this
107
+ }
108
+
109
+ public withExpress(existingExpress: Express): this {
110
+ this.existingExpress = existingExpress
111
+ this._startListen = false
112
+ return this
113
+ }
114
+
115
+ public withCorsConfigurer(configurer: ExpressCorsConfigurer): this {
116
+ this._corsConfigurer = configurer
117
+ return this
118
+ }
119
+
120
+ public withPassportAuth(usePassport: boolean, initializeOptions?: InitializeOptions): this {
121
+ this._usePassportAuth = usePassport
122
+ this._passportInitOpts = initializeOptions
123
+ return this
124
+ }
125
+
126
+ public withGlobalUserIsInRole(userIsInRole: string | string[]): this {
127
+ this._userIsInRole = userIsInRole
128
+ return this
129
+ }
130
+
131
+ public withEnforcer(enforcer: Enforcer): this {
132
+ this._enforcer = enforcer
133
+ return this
134
+ }
135
+
136
+ public startListening(express: Express) {
137
+ return express.listen(this.getPort(), this.getHostname(), this.listenCallback)
138
+ }
139
+
140
+ public getHostname(): string {
141
+ return this.hostnameOrIP ?? env('HOSTNAME', this.envVarPrefix) ?? '0.0.0.0'
142
+ }
143
+
144
+ public getPort(): number {
145
+ return (this.port ?? env('PORT', this.envVarPrefix) ?? 5000) as number
146
+ }
147
+
148
+ public setHandlers(handlers: ApplicationRequestHandler<any> | ApplicationRequestHandler<any>[]): this {
149
+ if (Array.isArray(handlers)) {
150
+ this._handlers = handlers
151
+ } else if (handlers) {
152
+ if (!this._handlers) {
153
+ this._handlers = []
154
+ }
155
+ this._handlers.push(handlers)
156
+ } else {
157
+ this._handlers = []
158
+ }
159
+
160
+ return this
161
+ }
162
+
163
+ public addHandler(handler: ApplicationRequestHandler<any>): this {
164
+ if (!this._handlers) {
165
+ this._handlers = []
166
+ }
167
+ this._handlers.push(handler)
168
+ return this
169
+ }
170
+
171
+ public withSessionOptions(sessionOpts: session.SessionOptions): this {
172
+ this._sessionOpts = sessionOpts
173
+ return this
174
+ }
175
+
176
+ public build<T extends Application>(opts?: {
177
+ express?: Express
178
+ startListening?: boolean
179
+ handlers?: ApplicationRequestHandler<T> | ApplicationRequestHandler<T>[]
180
+ }): ExpressSupport {
181
+ const express = this.buildExpress(opts)
182
+ return {
183
+ express,
184
+ port: this.getPort(),
185
+ hostname: this.getHostname(),
186
+ userIsInRole: this._userIsInRole,
187
+ startListening: this._startListen !== false,
188
+ enforcer: this._enforcer,
189
+ start: (opts) => {
190
+ if (this._startListen !== false) {
191
+ this.startListening(express)
192
+ }
193
+
194
+ if (opts?.disableErrorHandler !== true) {
195
+ express.use(jsonErrorHandler)
196
+ }
197
+ return express
198
+ },
199
+ }
200
+ }
201
+
202
+ protected buildExpress<T extends Application>(opts?: {
203
+ express?: Express
204
+ startListening?: boolean
205
+ handlers?: ApplicationRequestHandler<T> | ApplicationRequestHandler<T>[]
206
+ }): express.Express {
207
+ const app: express.Express = opts?.express ?? this.existingExpress ?? express()
208
+ if (this._morgan) {
209
+ app.use(this._morgan)
210
+ }
211
+ if (this._sessionOpts) {
212
+ // @ts-ignore
213
+ app.use(session(this._sessionOpts))
214
+ }
215
+ if (this._usePassportAuth) {
216
+ app.use(passport.initialize(this._passportInitOpts))
217
+ if (this._sessionOpts) {
218
+ app.use(passport.session())
219
+ }
220
+ }
221
+ if (this._userIsInRole) {
222
+ app.use(checkUserIsInRole({ roles: this._userIsInRole }))
223
+ }
224
+ if (this._corsConfigurer) {
225
+ this._corsConfigurer.configure({ existingExpress: app })
226
+ }
227
+
228
+ // @ts-ignore
229
+ this._handlers && this._handlers.length > 0 && app.use(this._handlers)
230
+ // @ts-ignore
231
+ opts?.handlers && app.use(opts.handlers)
232
+
233
+ app.use(bodyParser.urlencoded({ extended: true }))
234
+ app.use(bodyParser.json())
235
+
236
+ /*if (this._startListen !== false) {
237
+ this.startListening(app)
238
+ }*/
239
+ return app
240
+ }
241
+ }
242
+
243
+ export class ExpressCorsConfigurer {
244
+ private _disableCors?: boolean
245
+ private _enablePreflightOptions?: boolean
246
+ private _allowOrigin?: boolean | string | RegExp | Array<boolean | string | RegExp>
247
+ private _allowMethods?: string | string[]
248
+ private _allowedHeaders?: string | string[]
249
+ private _allowCredentials?: boolean
250
+ private readonly _express?: Express
251
+ private readonly _envVarPrefix?: string
252
+
253
+ constructor({ existingExpress, envVarPrefix }: { existingExpress?: Express; envVarPrefix?: string }) {
254
+ this._express = existingExpress
255
+ this._envVarPrefix = envVarPrefix
256
+ }
257
+
258
+ public allowOrigin(value: string | boolean | RegExp | Array<string | boolean | RegExp>): this {
259
+ this._allowOrigin = value
260
+ return this
261
+ }
262
+
263
+ public disableCors(value: boolean): this {
264
+ this._disableCors = value
265
+ return this
266
+ }
267
+
268
+ public allowMethods(value: string | string[]): this {
269
+ this._allowMethods = value
270
+ return this
271
+ }
272
+
273
+ public allowedHeaders(value: string | string[]): this {
274
+ this._allowedHeaders = value
275
+ return this
276
+ }
277
+
278
+ public allowCredentials(value: boolean): this {
279
+ this._allowCredentials = value
280
+ return this
281
+ }
282
+
283
+ public configure({ existingExpress }: { existingExpress?: Express }) {
284
+ const express = existingExpress ?? this._express
285
+ if (!express) {
286
+ throw Error('No express passed in during construction or configure')
287
+ }
288
+
289
+ const disableCorsEnv = env('CORS_DISABLE', this._envVarPrefix)
290
+ const corsDisabled = this._disableCors ?? (disableCorsEnv ? /true/.test(disableCorsEnv) : false)
291
+ if (corsDisabled) {
292
+ return
293
+ }
294
+ const envAllowOriginStr = env('CORS_ALLOW_ORIGIN', this._envVarPrefix) ?? '*'
295
+ let envAllowOrigin: string[] | string
296
+ if (envAllowOriginStr.includes(',')) {
297
+ envAllowOrigin = envAllowOriginStr.split(',')
298
+ } else if (envAllowOriginStr.includes(' ')) {
299
+ envAllowOrigin = envAllowOriginStr.split(' ')
300
+ } else {
301
+ envAllowOrigin = envAllowOriginStr
302
+ }
303
+ if (Array.isArray(envAllowOrigin) && envAllowOrigin.length === 1) {
304
+ envAllowOrigin = envAllowOrigin[0]
305
+ }
306
+ const corsOptions: CorsOptions = {
307
+ origin: this._allowOrigin ?? envAllowOrigin,
308
+ // todo: env vars
309
+ ...(this._allowMethods && { methods: this._allowMethods }),
310
+ ...(this._allowedHeaders && { allowedHeaders: this._allowedHeaders }),
311
+ ...(this._allowCredentials !== undefined && { credentials: this._allowCredentials }),
312
+ optionsSuccessStatus: 204,
313
+ }
314
+
315
+ if (this._enablePreflightOptions) {
316
+ express.options('*', cors(corsOptions))
317
+ }
318
+ express.use(cors(corsOptions))
319
+ }
320
+ }
@@ -0,0 +1,30 @@
1
+ import express, { NextFunction } from 'express'
2
+ export function sendErrorResponse(response: express.Response, statusCode: number, message: string | object, error?: Error) {
3
+ console.log(`sendErrorResponse: ${message}`)
4
+ if (error) {
5
+ console.log(JSON.stringify(error))
6
+ }
7
+ if (response.headersSent) {
8
+ console.log(`sendErrorResponse headers already sent`)
9
+ return
10
+ }
11
+ response.statusCode = statusCode
12
+ if (typeof message === 'string' && !message.startsWith('{')) {
13
+ message = { error: message }
14
+ }
15
+ if (typeof message === 'string' && message.startsWith('{')) {
16
+ return response.status(statusCode).end(message)
17
+ }
18
+ return response.status(statusCode).json(message)
19
+ }
20
+
21
+ export const jsonErrorHandler = (err: any, req: express.Request, res: express.Response, next: NextFunction) => {
22
+ const statusCode: number = 'statusCode' in err ? err.statusCode : 500
23
+ const errorMsg = typeof err === 'string' ? err : err.message
24
+ if (res.headersSent) {
25
+ console.log('Headers already sent, when calling error handler. Will defer to next error handler')
26
+ console.log(`Error was: ${JSON.stringify(err)}`)
27
+ return next(err)
28
+ }
29
+ return sendErrorResponse(res, statusCode, errorMsg, typeof err !== 'string' ? err : undefined)
30
+ }
@@ -0,0 +1,6 @@
1
+ export function env(key?: string, prefix?: string): string | undefined {
2
+ if (!key) {
3
+ return
4
+ }
5
+ return process.env[`${prefix ? prefix.trim() : ''}${key}`]
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './entra-id-auth'
2
+ export * from './static-bearer-auth'
3
+ export * from './auth-utils'
4
+ export * from './express-builders'
5
+ export * from './types'
6
+ export { sendErrorResponse, jsonErrorHandler } from './express-utils'
7
+ export * from './functions'
@@ -0,0 +1,47 @@
1
+ import passport from 'passport'
2
+ import { IBearerStrategyOption, IBearerStrategyOptionWithRequest, ITokenPayload, VerifyCallback } from './types'
3
+
4
+ export class OpenIDConnectAuth {
5
+ private readonly strategy: string
6
+ private options?: IBearerStrategyOptionWithRequest
7
+
8
+ public static init(strategy: string) {
9
+ return new OpenIDConnectAuth(strategy)
10
+ }
11
+
12
+ private constructor(strategy: string) {
13
+ this.strategy = strategy
14
+ }
15
+
16
+ public withOptions(options: IBearerStrategyOption | IBearerStrategyOptionWithRequest): this {
17
+ this.options = {
18
+ ...options,
19
+ passReqToCallback: 'passReqToCallback' in options ? options.passReqToCallback : false,
20
+ }
21
+ return this
22
+ }
23
+
24
+ connectPassport() {
25
+ const _options = this.options
26
+ if (!_options) {
27
+ throw Error('No options supplied for EntraID')
28
+ }
29
+ import('passport-azure-ad')
30
+ .then((entraID) =>
31
+ passport.use(
32
+ this.strategy,
33
+ new entraID.BearerStrategy(_options, function (token: ITokenPayload, cb: VerifyCallback): void {
34
+ if (token) {
35
+ // console.log(`token: ${JSON.stringify(token, null, 2)}`)
36
+ return cb(null, token)
37
+ }
38
+ return cb('bearer token not found or incorrect', null)
39
+ })
40
+ )
41
+ )
42
+ .catch((reason) => {
43
+ console.log(reason)
44
+ throw Error('Could not create bearer strategy. Did you include the "passport-azure-ad/bearer-strategy" dependency in package.json?')
45
+ })
46
+ }
47
+ }
@@ -0,0 +1,151 @@
1
+ import passport from 'passport'
2
+ import * as u8a from 'uint8arrays'
3
+ import { BearerUser, IStaticBearerVerifyOptions } from './types'
4
+ export class StaticBearerAuth {
5
+ private readonly strategy: string
6
+ private static providers: Map<string, StaticBearerUserProvider> = new Map()
7
+ private static verifyOptions: Map<string, IStaticBearerVerifyOptions | string> = new Map()
8
+ private hashTokens?: boolean = false
9
+
10
+ public static init(strategy: string, provider?: StaticBearerUserProvider) {
11
+ return new StaticBearerAuth(strategy ?? 'bearer', provider ?? new MapBasedStaticBearerUserProvider(strategy))
12
+ }
13
+
14
+ private constructor(strategy: string, provider: StaticBearerUserProvider) {
15
+ this.strategy = strategy
16
+ if (StaticBearerAuth.providers.has(strategy)) {
17
+ if (StaticBearerAuth.providers.get(strategy) !== provider) {
18
+ throw Error('Cannot register another user provider for strategy: ' + strategy)
19
+ }
20
+ } else {
21
+ StaticBearerAuth.providers.set(strategy, provider)
22
+ }
23
+ }
24
+
25
+ get provider() {
26
+ const provider = StaticBearerAuth.providers.get(this.strategy)
27
+ if (!provider) {
28
+ throw Error('Could not get user provider for ' + this.strategy)
29
+ }
30
+ return provider
31
+ }
32
+
33
+ withHashTokens(hashTokens: boolean): this {
34
+ this.hashTokens = hashTokens
35
+ return this
36
+ }
37
+
38
+ withUsers(users: BearerUser[] | BearerUser): this {
39
+ this.addUser(users)
40
+ return this
41
+ }
42
+
43
+ addUser(user: BearerUser[] | BearerUser): this {
44
+ this.provider.addUser(user)
45
+ return this
46
+ }
47
+
48
+ withVerifyOptions(options: IStaticBearerVerifyOptions | string): this {
49
+ StaticBearerAuth.verifyOptions.set(this.strategy, options)
50
+ return this
51
+ }
52
+
53
+ connectPassport() {
54
+ const _provider = this.provider
55
+ function findUser(token: string, cb: (error: any, user: any, options?: IStaticBearerVerifyOptions | string) => void) {
56
+ const user = _provider.getUser(token)
57
+ if (user) {
58
+ return cb(null, user)
59
+ }
60
+ return cb('bearer token not found or incorrect', false)
61
+ }
62
+
63
+ import('passport-http-bearer')
64
+ .then((httpBearer) => {
65
+ const hashTokens = this.hashTokens ?? false
66
+ passport.use(
67
+ this.strategy,
68
+ new httpBearer.Strategy({ passReqToCallback: false }, function (
69
+ token: string,
70
+ cb: (error: any, user: any, options?: IStaticBearerVerifyOptions | string) => void
71
+ ): void {
72
+ if (hashTokens) {
73
+ import('@noble/hashes/sha256')
74
+ .then((hash) => {
75
+ findUser(u8a.toString(hash.sha256(token)), cb)
76
+ })
77
+ .catch((error) => {
78
+ console.log(`hash problem: ${error}`)
79
+ throw Error('Did you include @noble/hashes in package.json?')
80
+ })
81
+ } else {
82
+ findUser(token, cb)
83
+ }
84
+ })
85
+ )
86
+ })
87
+ .catch((error) => {
88
+ console.log(`passport-http-bearer package problem: ${error}`)
89
+ throw Error('Did you include passport-http-bearer in package.json?')
90
+ })
91
+ }
92
+ }
93
+
94
+ export interface StaticBearerUserProvider {
95
+ strategy: string
96
+
97
+ addUser(user: BearerUser | BearerUser[], hashToken?: boolean): void
98
+
99
+ getUser(token: string): BearerUser | undefined
100
+
101
+ hashedTokens?: boolean
102
+ }
103
+
104
+ export class MapBasedStaticBearerUserProvider implements StaticBearerUserProvider {
105
+ private readonly _strategy: string
106
+ private readonly _users: BearerUser[] = []
107
+ private readonly _hashedTokens: boolean
108
+
109
+ constructor(strategy: string, hashedTokens?: boolean) {
110
+ this._strategy = strategy
111
+ this._hashedTokens = hashedTokens ?? false
112
+ }
113
+
114
+ get users(): BearerUser[] {
115
+ return this._users
116
+ }
117
+
118
+ get hashedTokens(): boolean {
119
+ return this._hashedTokens
120
+ }
121
+
122
+ get strategy(): string {
123
+ return this._strategy
124
+ }
125
+
126
+ getUser(token: string): BearerUser | undefined {
127
+ return this.users.find((user) => user.token === token)
128
+ }
129
+
130
+ addUser(user: BearerUser | BearerUser[], hashToken?: boolean): void {
131
+ const users = Array.isArray(user) ? user : [user]
132
+ if (hashToken) {
133
+ if (!this.hashedTokens) {
134
+ throw Error('Cannot hash token, when hashed tokens is not enabled on the user provider for strategy ' + this.strategy)
135
+ }
136
+ import('@noble/hashes/sha256')
137
+ .then((hash) => {
138
+ users.forEach((user) => (user.token = u8a.toString(hash.sha256(user.token))))
139
+ })
140
+ .catch((error) => {
141
+ console.log(`hash problem: ${error}`)
142
+ throw Error('Did you include @noble/hashes in package.json?')
143
+ })
144
+ }
145
+ this._users.push(...users)
146
+ }
147
+
148
+ getUsers(): BearerUser[] {
149
+ return this._users
150
+ }
151
+ }