@owlmeans/server-oidc-rp 0.1.0

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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +520 -0
  3. package/build/.gitkeep +0 -0
  4. package/build/actions/index.d.ts +3 -0
  5. package/build/actions/index.d.ts.map +1 -0
  6. package/build/actions/index.js +3 -0
  7. package/build/actions/index.js.map +1 -0
  8. package/build/actions/init.d.ts +3 -0
  9. package/build/actions/init.d.ts.map +1 -0
  10. package/build/actions/init.js +68 -0
  11. package/build/actions/init.js.map +1 -0
  12. package/build/actions/process.d.ts +17 -0
  13. package/build/actions/process.d.ts.map +1 -0
  14. package/build/actions/process.js +80 -0
  15. package/build/actions/process.js.map +1 -0
  16. package/build/auth/consts.d.ts +2 -0
  17. package/build/auth/consts.d.ts.map +1 -0
  18. package/build/auth/consts.js +2 -0
  19. package/build/auth/consts.js.map +1 -0
  20. package/build/auth/index.d.ts +2 -0
  21. package/build/auth/index.d.ts.map +1 -0
  22. package/build/auth/index.js +2 -0
  23. package/build/auth/index.js.map +1 -0
  24. package/build/auth/plugins/index.d.ts +2 -0
  25. package/build/auth/plugins/index.d.ts.map +1 -0
  26. package/build/auth/plugins/index.js +6 -0
  27. package/build/auth/plugins/index.js.map +1 -0
  28. package/build/auth/plugins/oidc-client.d.ts +50 -0
  29. package/build/auth/plugins/oidc-client.d.ts.map +1 -0
  30. package/build/auth/plugins/oidc-client.js +211 -0
  31. package/build/auth/plugins/oidc-client.js.map +1 -0
  32. package/build/auth/plugins/types.d.ts +1 -0
  33. package/build/auth/plugins/types.d.ts.map +1 -0
  34. package/build/auth/plugins/types.js +2 -0
  35. package/build/auth/plugins/types.js.map +1 -0
  36. package/build/consts.d.ts +20 -0
  37. package/build/consts.d.ts.map +1 -0
  38. package/build/consts.js +20 -0
  39. package/build/consts.js.map +1 -0
  40. package/build/gate.d.ts +3 -0
  41. package/build/gate.d.ts.map +1 -0
  42. package/build/gate.js +22 -0
  43. package/build/gate.js.map +1 -0
  44. package/build/guard.d.ts +6 -0
  45. package/build/guard.d.ts.map +1 -0
  46. package/build/guard.js +13 -0
  47. package/build/guard.js.map +1 -0
  48. package/build/index.d.ts +8 -0
  49. package/build/index.d.ts.map +1 -0
  50. package/build/index.js +7 -0
  51. package/build/index.js.map +1 -0
  52. package/build/model/consts.d.ts +6 -0
  53. package/build/model/consts.d.ts.map +1 -0
  54. package/build/model/consts.js +7 -0
  55. package/build/model/consts.js.map +1 -0
  56. package/build/model/gate.d.ts +4 -0
  57. package/build/model/gate.d.ts.map +1 -0
  58. package/build/model/gate.js +98 -0
  59. package/build/model/gate.js.map +1 -0
  60. package/build/model/types.d.ts +25 -0
  61. package/build/model/types.d.ts.map +1 -0
  62. package/build/model/types.js +2 -0
  63. package/build/model/types.js.map +1 -0
  64. package/build/models.d.ts +1 -0
  65. package/build/models.d.ts.map +1 -0
  66. package/build/models.js +2 -0
  67. package/build/models.js.map +1 -0
  68. package/build/modules.d.ts +3 -0
  69. package/build/modules.d.ts.map +1 -0
  70. package/build/modules.js +12 -0
  71. package/build/modules.js.map +1 -0
  72. package/build/service.d.ts +3 -0
  73. package/build/service.d.ts.map +1 -0
  74. package/build/service.js +186 -0
  75. package/build/service.js.map +1 -0
  76. package/build/types.d.ts +55 -0
  77. package/build/types.d.ts.map +1 -0
  78. package/build/types.js +2 -0
  79. package/build/types.js.map +1 -0
  80. package/build/utils/auth.d.ts +5 -0
  81. package/build/utils/auth.d.ts.map +1 -0
  82. package/build/utils/auth.js +48 -0
  83. package/build/utils/auth.js.map +1 -0
  84. package/build/utils/cache.d.ts +8 -0
  85. package/build/utils/cache.d.ts.map +1 -0
  86. package/build/utils/cache.js +7 -0
  87. package/build/utils/cache.js.map +1 -0
  88. package/build/utils/index.d.ts +5 -0
  89. package/build/utils/index.d.ts.map +1 -0
  90. package/build/utils/index.js +4 -0
  91. package/build/utils/index.js.map +1 -0
  92. package/build/utils/types.d.ts +10 -0
  93. package/build/utils/types.d.ts.map +1 -0
  94. package/build/utils/types.js +2 -0
  95. package/build/utils/types.js.map +1 -0
  96. package/build/utils/wrapped.d.ts +4 -0
  97. package/build/utils/wrapped.d.ts.map +1 -0
  98. package/build/utils/wrapped.js +3 -0
  99. package/build/utils/wrapped.js.map +1 -0
  100. package/build/wrapper.d.ts +3 -0
  101. package/build/wrapper.d.ts.map +1 -0
  102. package/build/wrapper.js +75 -0
  103. package/build/wrapper.js.map +1 -0
  104. package/package.json +71 -0
  105. package/src/actions/index.ts +3 -0
  106. package/src/actions/init.ts +90 -0
  107. package/src/actions/process.ts +113 -0
  108. package/src/auth/consts.ts +2 -0
  109. package/src/auth/index.ts +2 -0
  110. package/src/auth/plugins/index.ts +8 -0
  111. package/src/auth/plugins/oidc-client.ts +246 -0
  112. package/src/auth/plugins/types.ts +0 -0
  113. package/src/consts.ts +26 -0
  114. package/src/gate.ts +29 -0
  115. package/src/guard.ts +25 -0
  116. package/src/index.ts +7 -0
  117. package/src/model/consts.ts +6 -0
  118. package/src/model/gate.ts +129 -0
  119. package/src/model/types.ts +31 -0
  120. package/src/models.ts +1 -0
  121. package/src/modules.ts +30 -0
  122. package/src/service.ts +251 -0
  123. package/src/types.ts +64 -0
  124. package/src/utils/auth.ts +67 -0
  125. package/src/utils/cache.ts +12 -0
  126. package/src/utils/index.ts +5 -0
  127. package/src/utils/types.ts +10 -0
  128. package/src/utils/wrapped.ts +6 -0
  129. package/src/wrapper.ts +92 -0
  130. package/tsconfig.json +15 -0
  131. package/tsconfig.tsbuildinfo +1 -0
package/src/consts.ts ADDED
@@ -0,0 +1,26 @@
1
+
2
+ export const DEFAULT_ALIAS = 'oidc-client'
3
+
4
+ export const DEF_OIDC_ACCOUNT_LINKING = 'oidc-consumer-account-linking'
5
+
6
+ export const DEF_OIDC_PROVIDER_API = 'oidc-consumer-provider-api'
7
+
8
+ export const authService = {
9
+ provider: {
10
+ list: 'external-auth:provider:list'
11
+ },
12
+ auth: {
13
+ update: 'external-auth:auth:update'
14
+ }
15
+ }
16
+
17
+ export const OIDC_TOKEN_STORE = 'oidc-token-store'
18
+ /**
19
+ * @TODO Actually these ones should be constants and should be linked to
20
+ * configuration on OIDC provider side
21
+ */
22
+ export const PROVIDER_CACHE_TTL = (5 * 60 - 1) * 1000
23
+
24
+ export const OIDC_AUTH_LIFTETIME = 24 * 3600 * 1000
25
+
26
+ export const OIDC_WRAP_FRESHNESS = (5 * 60 - 1) * 1000
package/src/gate.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { createLazyService } from '@owlmeans/context'
2
+ import type { GateService } from '@owlmeans/module'
3
+ import { OIDC_GATE } from '@owlmeans/oidc'
4
+ import type { Config, Context } from './types.js'
5
+ import { AuthForbidden } from '@owlmeans/auth'
6
+ import { createGateModel } from './model/gate.js'
7
+
8
+ export const makeOidcGate = (alias: string = OIDC_GATE): GateService => {
9
+ const service: GateService = createLazyService<GateService>(alias, {
10
+ assert: async (req, _, params) => {
11
+ await service.ready()
12
+ const ctx = service.assertCtx<Config, Context>()
13
+
14
+ if (req.auth == null) {
15
+ throw new AuthForbidden('auth')
16
+ }
17
+
18
+ const model = createGateModel(ctx)
19
+
20
+ const permissions = await model.loadPermissions(req.auth, params)
21
+
22
+ if (permissions.length < 1) {
23
+ throw new AuthForbidden('permission')
24
+ }
25
+ }
26
+ })
27
+
28
+ return service
29
+ }
package/src/guard.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { OidcGuardOptions } from '@owlmeans/oidc'
2
+ import type { Config, Context } from './types.js'
3
+ import type { CommonModule } from '@owlmeans/module'
4
+ import {
5
+ appendOidcGuard as appendBasicOidcGuard,
6
+ setupOidcGuard as setupBasicOidcGuard,
7
+ DISPATCHER_OIDC, DISPATCHER_OIDC_INIT
8
+ } from '@owlmeans/oidc'
9
+ import { elevate } from '@owlmeans/server-module'
10
+ import * as actions from './actions/index.js'
11
+
12
+ export const appendOidcGuard = <C extends Config, T extends Context<C>>(
13
+ context: T, opts?: OidcGuardOptions
14
+ ) => {
15
+ const ctx = appendBasicOidcGuard<C, T>(context, opts)
16
+
17
+ return ctx
18
+ }
19
+
20
+ export const setupOidcGuard = (modules: CommonModule[], coguards?: string | string[]) => {
21
+ setupBasicOidcGuard(modules, coguards)
22
+
23
+ elevate(modules, DISPATCHER_OIDC_INIT, actions.init)
24
+ elevate(modules, DISPATCHER_OIDC, actions.authenticate)
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type * from './types.js'
2
+ export * from './consts.js'
3
+ export * from './service.js'
4
+ export * from './guard.js'
5
+ export * from './gate.js'
6
+ export * from './modules.js'
7
+ export * from './wrapper.js'
@@ -0,0 +1,6 @@
1
+
2
+ export enum ResponseMode {
3
+ Permissions = 'permissions',
4
+ Token = 'token',
5
+ Decision = 'decision',
6
+ }
@@ -0,0 +1,129 @@
1
+ import { AuthForbidden, AuthManagerError, AuthManagerUnsupported, AuthUnknown } from '@owlmeans/auth'
2
+ import { authService, DEFAULT_ALIAS } from '../consts.js'
3
+ import type { Config, Context, OidcClientService } from '../types.js'
4
+ import { cache, managedId } from '../utils/cache.js'
5
+ import type { GateModel, PermissionRequest, PermissionResponse } from './types.js'
6
+ import { ResponseMode } from './consts.js'
7
+ import type { ClientModule } from '@owlmeans/client-module'
8
+ import type { OidcProviderDescriptor } from '@owlmeans/oidc'
9
+
10
+ export const createGateModel = <C extends Config, T extends Context<C>>(ctx: T): GateModel => {
11
+ const model: GateModel = {
12
+ getClient: async user => {
13
+ const oidc = ctx.service<OidcClientService>(DEFAULT_ALIAS)
14
+ try {
15
+ return await oidc.getClient({
16
+ entityId: user.entityId,
17
+ clientId: oidc.entityToClientId({ entityId: user.entityId })
18
+ })
19
+ } catch (e) {
20
+ if (e instanceof AuthManagerError) {
21
+ const [providers] = await ctx.module<ClientModule<OidcProviderDescriptor[]>>(
22
+ authService.provider.list
23
+ ).call({
24
+ params: { service: ctx.cfg.alias ?? ctx.cfg.service },
25
+ query: { entityId: user.entityId }
26
+ })
27
+ if (providers.length < 1) {
28
+ throw new AuthUnknown()
29
+ }
30
+ oidc.registerTemporaryProvider(providers[0])
31
+
32
+ return model.getClient(user)
33
+ }
34
+ throw e
35
+ }
36
+ },
37
+
38
+ loadPermissions: async (user, permissions) => {
39
+ const record = await cache<C, T>(ctx).get(managedId(user.token))
40
+ if (record.payload == null) {
41
+ throw new AuthForbidden('record')
42
+ }
43
+
44
+ const client = await model.getClient(user)
45
+
46
+ const metadata = client.getMetadata()
47
+ const tokenEndpoint = metadata.token_endpoint
48
+
49
+ if (tokenEndpoint == null) {
50
+ throw new AuthManagerUnsupported('token-url')
51
+ }
52
+
53
+ const request = await model.makeRequest(
54
+ model.fixPermissions(permissions, user),
55
+ client
56
+ )
57
+
58
+ request.subject_token = record.payload.access_token
59
+
60
+ const response = await fetch(
61
+ tokenEndpoint,
62
+ await model.prepareRequest(client, request)
63
+ )
64
+
65
+ if (!response.ok) {
66
+ console.warn(await response.text())
67
+ return []
68
+ }
69
+
70
+ const proofs = await response.json() as PermissionResponse[]
71
+
72
+ return permissions.filter(permission => {
73
+ const [fixed] = model.fixPermissions([permission], user)
74
+ const [resource, scope] = fixed.split(':')
75
+ const proof = proofs.find(p => p.rsid === resource || p.rsname === resource)
76
+ return proof != null
77
+ && (scope == null || (proof.scopes?.includes(scope) ?? false))
78
+ })
79
+ },
80
+
81
+ fixPermissions: (permissions, user) => permissions.map(
82
+ perm => perm.replaceAll('{entity}', user.entityId ?? '-')
83
+ ),
84
+
85
+ prepareRequest: async (client, request) => {
86
+ let { permission, ..._request } = request
87
+ const params = new URLSearchParams(_request)
88
+ permission = Array.isArray(permission) ? permission : [permission]
89
+ permission.forEach(perm => params.append('permission', perm))
90
+ const body = params.toString()
91
+
92
+ const cfg = client.getConfig()
93
+
94
+ const headers = {
95
+ 'Content-Type': 'application/x-www-form-urlencoded',
96
+ 'Authorization': `Basic ${Buffer.from(`${client.getClientId()}:${cfg.secret}`).toString('base64')}`,
97
+ 'Content-Length': `${body.length}`,
98
+ }
99
+
100
+ return { body, headers, method: 'POST' }
101
+ },
102
+
103
+ makeRequest: async (permissions, client) => {
104
+ const cfg = client.getConfig()
105
+ const clientId = client.getClientId()
106
+ const permission = {
107
+ grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
108
+ audience: clientId,
109
+ client_id: clientId,
110
+ secret: cfg.secret,
111
+ permission: permissions.reduce((output, permission) => {
112
+ const [resource, scope] = permission.split(':')
113
+ const idx = output.findIndex(perm => perm.startsWith(resource))
114
+ if (idx > -1) {
115
+ output[idx] += `, ${scope}`
116
+ } else {
117
+ output.push(`${resource}${scope ? `#${scope}` : ''}`)
118
+ }
119
+ return output
120
+ }, [] as string[]),
121
+ response_mode: ResponseMode.Permissions,
122
+ } satisfies PermissionRequest
123
+
124
+ return permission
125
+ }
126
+ }
127
+
128
+ return model
129
+ }
@@ -0,0 +1,31 @@
1
+ import type { Auth } from '@owlmeans/auth'
2
+ import type { OidcClientAdapter } from '../types.js'
3
+ import type { ResponseMode } from './consts.js'
4
+
5
+ export interface GateModel {
6
+ getClient: (user: Auth) => Promise<OidcClientAdapter>
7
+
8
+ loadPermissions: (user: Auth, permissions: string[]) => Promise<string[]>
9
+
10
+ fixPermissions: (permissions: string[], user: Auth) => string[]
11
+
12
+ prepareRequest: (client: OidcClientAdapter, request: PermissionRequest) => Promise<RequestInit>
13
+
14
+ makeRequest: (permissions: string[], client: OidcClientAdapter) => Promise<PermissionRequest>
15
+ }
16
+
17
+ export interface PermissionRequest {
18
+ grant_type: string
19
+ audience: string
20
+ client_id?: string
21
+ secret?: string
22
+ subject_token?: string
23
+ permission: string | string[]
24
+ response_mode: ResponseMode
25
+ }
26
+
27
+ export interface PermissionResponse {
28
+ rsid: string
29
+ rsname: string
30
+ scopes?: string[]
31
+ }
package/src/models.ts ADDED
@@ -0,0 +1 @@
1
+
package/src/modules.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { CommonModule } from '@owlmeans/module'
2
+ import { module, guard, filter, body } from '@owlmeans/module'
3
+ import { backend, route, RouteMethod, service } from '@owlmeans/route'
4
+ import { authService } from './consts.js'
5
+ import { elevate } from '@owlmeans/client-module'
6
+ import { GUARD_ED25519 } from '@owlmeans/auth-common'
7
+ import { OIDCTokenUpdateSchema } from '@owlmeans/oidc'
8
+
9
+ export const setupAuthServiceModules = (
10
+ modules: CommonModule[],
11
+ serviceAlias: string,
12
+ prefix: string = 'oidc-api'
13
+ ) => {
14
+ modules.push(
15
+ module(
16
+ route(authService.provider.list, `/${prefix}/provider/:service`, service(serviceAlias)),
17
+ guard(GUARD_ED25519)
18
+ ),
19
+ module(
20
+ route(
21
+ authService.auth.update, `/${prefix}/auth/update`,
22
+ backend(service(serviceAlias), RouteMethod.POST)
23
+ ),
24
+ filter(body(OIDCTokenUpdateSchema), guard(GUARD_ED25519))
25
+ )
26
+ )
27
+
28
+ elevate(modules, authService.provider.list)
29
+ elevate(modules, authService.auth.update)
30
+ }
package/src/service.ts ADDED
@@ -0,0 +1,251 @@
1
+ import { DEF_OIDC_ACCOUNT_LINKING, DEF_OIDC_PROVIDER_API, DEFAULT_ALIAS } from './consts.js'
2
+ import { assertContext, createService } from '@owlmeans/context'
3
+ import type { AccountLinkingService, Config, Context, OidcClientService, ProviderApiService, OidcClientAdapter } from './types.js'
4
+ import { AuthManagerError } from '@owlmeans/auth'
5
+ import { makeSecurityHelper } from '@owlmeans/config'
6
+ import * as client from 'openid-client'
7
+ import type { OidcProviderConfig } from '@owlmeans/oidc'
8
+ import { Configuration } from 'openid-client'
9
+ // import { URL as SrvURL } from 'node:url'
10
+
11
+ export const makeOidcClientService = (alias: string = DEFAULT_ALIAS): OidcClientService => {
12
+ const service: OidcClientService = createService<OidcClientService>(alias, {
13
+ getConfiguration: async clientId => {
14
+ const context = assertContext<Config, Context>(service.ctx as Context, alias)
15
+ let cfg = await service.getConfig(clientId)
16
+
17
+ if (cfg?.basePath == null) {
18
+ throw new AuthManagerError('oidc.client.basepath')
19
+ }
20
+
21
+ if (cfg.service == null) {
22
+ throw new AuthManagerError('oidc.client.service')
23
+ }
24
+
25
+ if (typeof clientId === 'object' && clientId.clientId == null) {
26
+ throw new AuthManagerError('oidc.client.client-id')
27
+ }
28
+
29
+ // @TODO Add support for any domain (not just registered services)
30
+ const security = makeSecurityHelper<Config, Context>(context)
31
+ const url = cfg.discoveryUrl
32
+ ?? security.makeUrl(context.cfg.services[cfg.service], cfg.basePath)
33
+
34
+ return await client.discovery(
35
+ new URL(url) as URL,
36
+ cfg.idOverride ?? (
37
+ typeof clientId === 'string' ? clientId : clientId.clientId!
38
+ ),
39
+ cfg.secret, undefined, {
40
+ execute: [
41
+ // @TODO Properly handle it with the config
42
+ client.allowInsecureRequests
43
+ ]
44
+ })
45
+ },
46
+
47
+ getClient: async clientId => {
48
+ let params: string | OidcProviderConfig
49
+ let descriptor: Configuration | null = null
50
+ let _clientId: string
51
+ if (clientId instanceof Configuration) {
52
+ descriptor = clientId
53
+ _clientId = params = clientId.serverMetadata().issuer
54
+ } else if (typeof clientId === 'string') {
55
+ _clientId = params = clientId
56
+ } else {
57
+ params = clientId as OidcProviderConfig
58
+ _clientId = params.clientId
59
+ }
60
+ let metadata = await descriptor?.serverMetadata()
61
+ // @TODO Do not forget to delete it
62
+ if (_clientId == null) {
63
+ throw new AuthManagerError('oidc.client.client-id')
64
+ }
65
+
66
+ descriptor ??= await service.getConfiguration(params)
67
+
68
+ const cfg = await service.getConfig(params)
69
+ if (cfg?.secret == null) {
70
+ throw new AuthManagerError('oidc.client.secert')
71
+ }
72
+
73
+ metadata ??= await descriptor.serverMetadata()
74
+
75
+ // return new issuer.Client({ client_id: _clientId, client_secret: cfg?.secret })
76
+ return {
77
+ getMetadata: () => metadata,
78
+
79
+ getClientId: () => cfg.idOverride ?? _clientId,
80
+
81
+ getConfig: () => cfg,
82
+
83
+ makeAuthUrl: params => client.buildAuthorizationUrl(descriptor, params).toString(),
84
+
85
+ // grantWithCredentials: async () => client.clientCredentialsGrant(descriptor, { grant_type: 'client_credentials' }),
86
+ grantWithCredentials: async () => client.clientCredentialsGrant(descriptor),
87
+
88
+ grantWithCode: async (url, checks, params) => {
89
+ const urlObj = new URL(url)
90
+ Object.entries(params).forEach(([key, value]) => {
91
+ if (key === 'flow') {
92
+ return
93
+ }
94
+
95
+ urlObj.searchParams.set(key, value)
96
+ })
97
+
98
+ checks.idTokenExpected ??= true
99
+
100
+ return await client.authorizationCodeGrant(
101
+ descriptor, urlObj, checks
102
+ )
103
+ },
104
+
105
+ refresh: async tokenSet => {
106
+ const refreshToken = typeof tokenSet === 'string' ? tokenSet : tokenSet.refresh_token
107
+ if (refreshToken == null) {
108
+ throw new AuthManagerError('refresh-token')
109
+ }
110
+ return await client.refreshTokenGrant(descriptor, refreshToken)
111
+ },
112
+
113
+ introspect: async (tokenSet, type = 'access_token') =>
114
+ await client.tokenIntrospection(descriptor, tokenSet[type] as string),
115
+
116
+ } satisfies OidcClientAdapter
117
+ },
118
+
119
+ getConfig: async clientId => {
120
+ const context = assertContext<Config, Context>(service.ctx as Context, alias)
121
+
122
+ if (typeof clientId === 'object' && clientId.clientId == null) {
123
+ throw new AuthManagerError('oidc.client.client-id')
124
+ }
125
+
126
+ return context.cfg.oidc.providers?.find(consumer => {
127
+ if (typeof clientId === 'string') {
128
+ return consumer.clientId === clientId
129
+ }
130
+ return Object.entries(clientId).every(
131
+ ([key, value]) => consumer[key as keyof typeof consumer] === value
132
+ )
133
+ })
134
+ },
135
+
136
+ getDefault: () => {
137
+ const context = assertContext<Config, Context>(service.ctx as Context, alias)
138
+
139
+ return context.cfg.oidc.providers?.find(consumer => consumer.def)?.clientId
140
+ },
141
+
142
+ providerApi: () => {
143
+ const context = assertContext<Config, Context>(service.ctx as Context, alias)
144
+ const cfg = context.cfg.oidc
145
+ const providerApiService = cfg.providerApiService ?? DEF_OIDC_PROVIDER_API
146
+ if (context.hasService(providerApiService)) {
147
+ return context.service<ProviderApiService>(providerApiService)
148
+ }
149
+ return null
150
+ },
151
+
152
+ accountLinking: () => {
153
+ const context = assertContext<Config, Context>(service.ctx as Context, alias)
154
+ const cfg = context.cfg.oidc
155
+ const accountLinkingService = cfg.accountLinkingService ?? DEF_OIDC_ACCOUNT_LINKING
156
+ if (context.hasService(accountLinkingService)) {
157
+ return context.service<AccountLinkingService>(accountLinkingService)
158
+ }
159
+ return null
160
+ },
161
+
162
+ hasProvider: params => {
163
+ const ctx = service.assertCtx<Config, Context>()
164
+ if (ctx.cfg.oidc.providers == null) {
165
+ ctx.cfg.oidc.providers = []
166
+ }
167
+
168
+ return ctx.cfg.oidc.providers.some(provider => {
169
+ if (typeof params === 'string') {
170
+ return provider.clientId === params
171
+ }
172
+ return Object.entries(params).every(
173
+ ([key, value]) => provider[key as keyof typeof provider] === value
174
+ )
175
+ })
176
+ },
177
+
178
+ registerTemporaryProvider: config => {
179
+ const ctx = service.assertCtx<Config, Context>()
180
+ if (ctx.cfg.oidc.providers == null) {
181
+ ctx.cfg.oidc.providers = []
182
+ }
183
+ const providers = ctx.cfg.oidc.providers as TemporaryConfig[]
184
+ let provider = providers.find(provider => matchCriteria.every(
185
+ key => provider[key as keyof typeof provider] === config[key as keyof typeof config]
186
+ ))
187
+ if (provider == null) {
188
+ provider = { ...config, [_configFlag]: 0 }
189
+ providers.push(provider)
190
+ } else {
191
+ Object.assign(provider, config)
192
+ provider[_configFlag] ??= 0
193
+ provider[_configFlag]++
194
+ }
195
+
196
+ return provider
197
+ },
198
+
199
+ entityToClientId: params => {
200
+ const ctx = service.assertCtx<Config, Context>()
201
+ if (ctx.cfg.oidc.providers == null) {
202
+ ctx.cfg.oidc.providers = []
203
+ }
204
+
205
+ const provider = ctx.cfg.oidc.providers.find(
206
+ provider => Object.entries(params).every(
207
+ ([key, value]) => provider[key as keyof typeof provider] === value
208
+ )
209
+ )
210
+ if (provider == null) {
211
+ throw new AuthManagerError('oidc.client.entity-id')
212
+ }
213
+
214
+ return provider.clientId
215
+ },
216
+
217
+ unregisterTemporaryProvider: params => {
218
+ const ctx = service.assertCtx<Config, Context>()
219
+ if (ctx.cfg.oidc.providers == null) {
220
+ ctx.cfg.oidc.providers = []
221
+ }
222
+ const providers = ctx.cfg.oidc.providers as TemporaryConfig[]
223
+
224
+ const idx = providers.findIndex(
225
+ provider => matchCriteria.every(
226
+ key => !(key in params) ||
227
+ provider[key as keyof typeof provider] === params[key as keyof typeof params]
228
+ )
229
+ )
230
+ if (idx < 0) {
231
+ return
232
+ }
233
+ const provider = providers[idx]
234
+
235
+ if (--provider[_configFlag] < 1) {
236
+ providers.splice(idx, 1)
237
+ }
238
+ }
239
+ })
240
+
241
+ return service
242
+ }
243
+
244
+ const matchCriteria = [
245
+ 'clientId', 'service', 'targetService', 'basePath', 'entityId'
246
+ ]
247
+
248
+ const _configFlag = Symbol('temporary-oidc-proivder-config')
249
+ interface TemporaryConfig extends OidcProviderConfig {
250
+ [_configFlag]: number
251
+ }
package/src/types.ts ADDED
@@ -0,0 +1,64 @@
1
+ import type { InitializedService } from '@owlmeans/context'
2
+ import type { AuthorizationCodeGrantChecks, Configuration, IntrospectionResponse, ServerMetadata, TokenEndpointResponse, TokenEndpointResponseHelpers } from 'openid-client'
3
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
4
+ import type { OidcProviderConfig, OidcSharedConfig, WithSharedConfig, ProviderProfileDetails, OidcUserDetails, OidcProviderSettings } from '@owlmeans/oidc'
5
+ import type { AuthCredentials, AuthPayload, Profile } from '@owlmeans/auth'
6
+
7
+ export type OidcClientDescriptor = Configuration
8
+ export type TokenSet = TokenEndpointResponse & TokenEndpointResponseHelpers
9
+ export type TokenSetParameters = TokenEndpointResponse
10
+
11
+ export interface OidcClientService extends InitializedService {
12
+ getConfiguration: (clientId: string | Partial<OidcProviderConfig>) => Promise<OidcClientDescriptor>
13
+ getClient: (clientId: string | OidcClientDescriptor | Partial<OidcProviderConfig>) => Promise<OidcClientAdapter>
14
+ getConfig: (clientId: string | Partial<OidcProviderConfig>) => Promise<OidcProviderConfig | undefined>
15
+ getDefault: () => string | undefined
16
+
17
+ registerTemporaryProvider: (config: OidcProviderConfig) => OidcProviderConfig
18
+ unregisterTemporaryProvider: (params: Partial<OidcProviderConfig>) => void
19
+ hasProvider: (params: Partial<OidcProviderConfig>) => boolean
20
+ entityToClientId: (params: Partial<OidcProviderConfig>) => string
21
+
22
+ providerApi: () => ProviderApiService | null
23
+ accountLinking: () => AccountLinkingService | null
24
+ }
25
+
26
+ export interface OidcClientAdapter {
27
+ getMetadata: () => ServerMetadata
28
+ getClientId: () => string
29
+ getConfig: () => OidcProviderConfig
30
+ makeAuthUrl: (params: Record<string, string>) => string
31
+ grantWithCredentials: () => Promise<TokenSet>
32
+ grantWithCode: (currentUrl: string, checks: AuthorizationCodeGrantChecks, params: Record<string, string>) => Promise<TokenSet>
33
+ refresh: (tokenSet: TokenSetParameters | string) => Promise<TokenSetParameters>
34
+ introspect: (tokenSet: TokenSetParameters, type?: string) => Promise<IntrospectionResponse>
35
+ }
36
+
37
+ export interface OidcRpConfig extends OidcSharedConfig {
38
+ accountLinkingService?: string
39
+ providerApiService?: string
40
+ }
41
+
42
+ export interface Config extends ServerConfig, WithSharedConfig {
43
+ oidc: OidcRpConfig
44
+ }
45
+
46
+ export interface Context<C extends Config = Config> extends ServerContext<C> { }
47
+
48
+ export interface AccountLinkingService extends InitializedService {
49
+ getLinkedProfile: (details: ProviderProfileDetails) => Promise<AuthPayload | null>
50
+ linkProfile: (details: ProviderProfileDetails, meta: AccountMeta) => Promise<AuthPayload>
51
+ linkCredentials: (details: ProviderProfileDetails) => Promise<AuthPayload>
52
+ getOwnerProfiles: (entityId: string) => Promise<Profile[]>
53
+ getOwnerCredentials: (userId: string, entityId?: string, type?: string) => Promise<AuthCredentials | undefined>
54
+ }
55
+
56
+ export interface AccountMeta {
57
+ username: string
58
+ force?: boolean
59
+ }
60
+
61
+ export interface ProviderApiService extends InitializedService {
62
+ getUserDetails: (token: string, userId: string) => Promise<OidcUserDetails>
63
+ getSettings: (token: string, realm: string) => Promise<OidcProviderSettings>
64
+ }
@@ -0,0 +1,67 @@
1
+ import type { AuthCredentials } from '@owlmeans/auth'
2
+ import { AuthenFailed, AuthenPayloadError } from '@owlmeans/auth'
3
+ import type { Config, Context, OidcClientService, TokenSet } from '../types.js'
4
+ import type { OidcProviderConfig } from '@owlmeans/oidc'
5
+ import type { } from 'openid-client'
6
+ import Url from 'url'
7
+ import { cache, exchangeId, verifierId } from './cache.js'
8
+ import { DEFAULT_ALIAS } from '../consts.js'
9
+ import { base64 } from '@scure/base'
10
+ import { randomBytes } from '@noble/hashes/utils'
11
+ import { AUTHEN_TIMEFRAME } from '@owlmeans/server-auth'
12
+
13
+ export const makeOidcAuthentication = <C extends Config, T extends Context<C>>(context: T) =>
14
+ async (credential: AuthCredentials): Promise<[OidcProviderConfig, TokenSet, string]> => {
15
+ console.log('Challenge we got: ', credential.challenge)
16
+ const challengeParts = credential.challenge.split(':http')
17
+ // This is ex. source - url we were going to return user to after all it happens
18
+ const redirectUrl = challengeParts[0]
19
+ // This is the auth service url with all necessary parameters that
20
+ // we sent user to for authentication.
21
+ const challengeUrl = new Url.URL('http' + challengeParts[1])
22
+ const challenge = challengeUrl.searchParams.get('code_challenge')
23
+ if (challenge == null) {
24
+ throw new AuthenPayloadError('code_challenge')
25
+ }
26
+
27
+ console.log("\n\nWe are picking verifier by id: ", verifierId(challenge))
28
+ const verification = await cache<C, T>(context).pick(verifierId(challenge))
29
+ console.log("Verification we get: ", verification, "\n\n")
30
+ if (verification.verifier == null) {
31
+ throw new AuthenFailed()
32
+ }
33
+ if (verification.client == null) {
34
+ throw new AuthenFailed()
35
+ }
36
+
37
+ const oidc = context.service<OidcClientService>(DEFAULT_ALIAS)
38
+ console.log(">>>>>>>> client we are trying to extract: ", verification)
39
+ const cfg = await oidc.getConfig({
40
+ clientId: verification.client,
41
+ ...(verification.entityId != null ? { entityId: verification.entityId } : {})
42
+ })
43
+ if (cfg == null) {
44
+ throw new AuthenFailed()
45
+ }
46
+
47
+ const client = await oidc.getClient(cfg)
48
+ const tokenSet = await client.grantWithCode(
49
+ redirectUrl,
50
+ { pkceCodeVerifier: verification.verifier },
51
+ {
52
+ ...Object.fromEntries(new Url.URLSearchParams(credential.credential).entries()),
53
+ }
54
+ )
55
+
56
+ // const params = client.callbackParams('/?' + credential.credential)
57
+
58
+ // const tokenSet = await client.callback(redirectUrl, params, { code_verifier: verification.verifier })
59
+
60
+ const exchangeToken = base64.encode(randomBytes(32))
61
+ await cache<C, T>(context).create(
62
+ { id: exchangeId(exchangeToken), payload: tokenSet },
63
+ { ttl: AUTHEN_TIMEFRAME / 1000 }
64
+ )
65
+
66
+ return [cfg, tokenSet, exchangeToken]
67
+ }