@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.
- package/LICENSE +21 -0
- package/README.md +520 -0
- package/build/.gitkeep +0 -0
- package/build/actions/index.d.ts +3 -0
- package/build/actions/index.d.ts.map +1 -0
- package/build/actions/index.js +3 -0
- package/build/actions/index.js.map +1 -0
- package/build/actions/init.d.ts +3 -0
- package/build/actions/init.d.ts.map +1 -0
- package/build/actions/init.js +68 -0
- package/build/actions/init.js.map +1 -0
- package/build/actions/process.d.ts +17 -0
- package/build/actions/process.d.ts.map +1 -0
- package/build/actions/process.js +80 -0
- package/build/actions/process.js.map +1 -0
- package/build/auth/consts.d.ts +2 -0
- package/build/auth/consts.d.ts.map +1 -0
- package/build/auth/consts.js +2 -0
- package/build/auth/consts.js.map +1 -0
- package/build/auth/index.d.ts +2 -0
- package/build/auth/index.d.ts.map +1 -0
- package/build/auth/index.js +2 -0
- package/build/auth/index.js.map +1 -0
- package/build/auth/plugins/index.d.ts +2 -0
- package/build/auth/plugins/index.d.ts.map +1 -0
- package/build/auth/plugins/index.js +6 -0
- package/build/auth/plugins/index.js.map +1 -0
- package/build/auth/plugins/oidc-client.d.ts +50 -0
- package/build/auth/plugins/oidc-client.d.ts.map +1 -0
- package/build/auth/plugins/oidc-client.js +211 -0
- package/build/auth/plugins/oidc-client.js.map +1 -0
- package/build/auth/plugins/types.d.ts +1 -0
- package/build/auth/plugins/types.d.ts.map +1 -0
- package/build/auth/plugins/types.js +2 -0
- package/build/auth/plugins/types.js.map +1 -0
- package/build/consts.d.ts +20 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +20 -0
- package/build/consts.js.map +1 -0
- package/build/gate.d.ts +3 -0
- package/build/gate.d.ts.map +1 -0
- package/build/gate.js +22 -0
- package/build/gate.js.map +1 -0
- package/build/guard.d.ts +6 -0
- package/build/guard.d.ts.map +1 -0
- package/build/guard.js +13 -0
- package/build/guard.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/model/consts.d.ts +6 -0
- package/build/model/consts.d.ts.map +1 -0
- package/build/model/consts.js +7 -0
- package/build/model/consts.js.map +1 -0
- package/build/model/gate.d.ts +4 -0
- package/build/model/gate.d.ts.map +1 -0
- package/build/model/gate.js +98 -0
- package/build/model/gate.js.map +1 -0
- package/build/model/types.d.ts +25 -0
- package/build/model/types.d.ts.map +1 -0
- package/build/model/types.js +2 -0
- package/build/model/types.js.map +1 -0
- package/build/models.d.ts +1 -0
- package/build/models.d.ts.map +1 -0
- package/build/models.js +2 -0
- package/build/models.js.map +1 -0
- package/build/modules.d.ts +3 -0
- package/build/modules.d.ts.map +1 -0
- package/build/modules.js +12 -0
- package/build/modules.js.map +1 -0
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +186 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +55 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/auth.d.ts +5 -0
- package/build/utils/auth.d.ts.map +1 -0
- package/build/utils/auth.js +48 -0
- package/build/utils/auth.js.map +1 -0
- package/build/utils/cache.d.ts +8 -0
- package/build/utils/cache.d.ts.map +1 -0
- package/build/utils/cache.js +7 -0
- package/build/utils/cache.js.map +1 -0
- package/build/utils/index.d.ts +5 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +4 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/types.d.ts +10 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils/wrapped.d.ts +4 -0
- package/build/utils/wrapped.d.ts.map +1 -0
- package/build/utils/wrapped.js +3 -0
- package/build/utils/wrapped.js.map +1 -0
- package/build/wrapper.d.ts +3 -0
- package/build/wrapper.d.ts.map +1 -0
- package/build/wrapper.js +75 -0
- package/build/wrapper.js.map +1 -0
- package/package.json +71 -0
- package/src/actions/index.ts +3 -0
- package/src/actions/init.ts +90 -0
- package/src/actions/process.ts +113 -0
- package/src/auth/consts.ts +2 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/plugins/index.ts +8 -0
- package/src/auth/plugins/oidc-client.ts +246 -0
- package/src/auth/plugins/types.ts +0 -0
- package/src/consts.ts +26 -0
- package/src/gate.ts +29 -0
- package/src/guard.ts +25 -0
- package/src/index.ts +7 -0
- package/src/model/consts.ts +6 -0
- package/src/model/gate.ts +129 -0
- package/src/model/types.ts +31 -0
- package/src/models.ts +1 -0
- package/src/modules.ts +30 -0
- package/src/service.ts +251 -0
- package/src/types.ts +64 -0
- package/src/utils/auth.ts +67 -0
- package/src/utils/cache.ts +12 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/types.ts +10 -0
- package/src/utils/wrapped.ts +6 -0
- package/src/wrapper.ts +92 -0
- package/tsconfig.json +15 -0
- 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,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
|
+
}
|