@owlmeans/auth-common 0.1.18-rc.9 → 0.1.18
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/README.md +31 -17
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/auth-common/SKILL.md +124 -23
- package/build/consts.d.ts +9 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +9 -0
- package/build/consts.js.map +1 -1
- package/build/entity/types.d.ts +52 -0
- package/build/entity/types.d.ts.map +1 -0
- package/build/entity/types.js +2 -0
- package/build/entity/types.js.map +1 -0
- package/build/entity.d.ts +5 -37
- package/build/entity.d.ts.map +1 -1
- package/build/entity.js +44 -65
- package/build/entity.js.map +1 -1
- package/build/entrypoints.d.ts +41 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +46 -0
- package/build/entrypoints.js.map +1 -0
- package/build/guards/basic-ed25519/consts.local.d.ts +3 -0
- package/build/guards/basic-ed25519/consts.local.d.ts.map +1 -0
- package/build/guards/basic-ed25519/consts.local.js +4 -0
- package/build/guards/basic-ed25519/consts.local.js.map +1 -0
- package/build/guards/basic-ed25519/index.d.ts +1 -0
- package/build/guards/basic-ed25519/index.d.ts.map +1 -1
- package/build/guards/basic-ed25519/index.js +1 -0
- package/build/guards/basic-ed25519/index.js.map +1 -1
- package/build/guards/basic-ed25519/replay.d.ts +14 -0
- package/build/guards/basic-ed25519/replay.d.ts.map +1 -0
- package/build/guards/basic-ed25519/replay.js +49 -0
- package/build/guards/basic-ed25519/replay.js.map +1 -0
- package/build/guards/basic-ed25519/service.d.ts +0 -5
- package/build/guards/basic-ed25519/service.d.ts.map +1 -1
- package/build/guards/basic-ed25519/service.js +34 -20
- package/build/guards/basic-ed25519/service.js.map +1 -1
- package/build/guards/basic-ed25519/types.d.ts +16 -0
- package/build/guards/basic-ed25519/types.d.ts.map +1 -1
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/middleware.d.ts +1 -1
- package/build/middleware.d.ts.map +1 -1
- package/build/middleware.js +13 -12
- package/build/middleware.js.map +1 -1
- package/build/modules.d.ts.map +1 -1
- package/build/modules.js +2 -11
- package/build/modules.js.map +1 -1
- package/build/schemas.d.ts +1 -13
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +13 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +5 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/trusted.d.ts.map +1 -1
- package/build/utils/trusted.js +4 -3
- package/build/utils/trusted.js.map +1 -1
- package/package.json +13 -13
- package/src/consts.ts +10 -0
- package/src/entity/types.ts +52 -0
- package/src/entity.ts +51 -66
- package/src/entrypoints.ts +68 -0
- package/src/guards/basic-ed25519/consts.local.ts +4 -0
- package/src/guards/basic-ed25519/index.ts +1 -0
- package/src/guards/basic-ed25519/replay.ts +55 -0
- package/src/guards/basic-ed25519/service.ts +44 -25
- package/src/guards/basic-ed25519/types.ts +17 -0
- package/src/index.ts +2 -1
- package/src/middleware.ts +15 -18
- package/src/schemas.ts +1 -14
- package/src/types.local.ts +5 -0
- package/src/types.ts +14 -0
- package/src/utils/trusted.ts +5 -4
- package/tests/basic-ed25519.spec.ts +99 -0
- package/tests/context.ts +1 -2
- package/tests/dispatcher.spec.ts +15 -15
- package/tests/entity.spec.ts +112 -0
- package/src/modules.ts +0 -65
package/src/schemas.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import type { JSONSchemaType } from 'ajv'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* What the surrogate login window is told about why it was opened.
|
|
5
|
-
*
|
|
6
|
-
* Every field is optional: the window must render something sensible when opened by hand, or by an
|
|
7
|
-
* older build that knew none of these parameters. `next` carries the address to run once the
|
|
8
|
-
* window is one level up, so the flow parameters the opener's URL held are forwarded rather than
|
|
9
|
-
* silently dropped.
|
|
10
|
-
*/
|
|
11
|
-
export interface SurrogateQuery {
|
|
12
|
-
intent?: string
|
|
13
|
-
next?: string
|
|
14
|
-
method?: string
|
|
15
|
-
}
|
|
2
|
+
import type { SurrogateQuery } from './types.js'
|
|
16
3
|
|
|
17
4
|
export const SurrogateQuerySchema: JSONSchemaType<SurrogateQuery> = {
|
|
18
5
|
type: 'object',
|
package/src/types.ts
CHANGED
|
@@ -97,3 +97,17 @@ export interface EntityResolverService extends InitializedService {
|
|
|
97
97
|
*/
|
|
98
98
|
mintName: (id: string, key: string, mint: (entity: OrgEntityRef) => string) => Promise<string>
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What the surrogate login window is told about why it was opened.
|
|
103
|
+
*
|
|
104
|
+
* Every field is optional: the window must render something sensible when opened by hand, or by an
|
|
105
|
+
* older build that knew none of these parameters. `next` carries the address to run once the
|
|
106
|
+
* window is one level up, so the flow parameters the opener's URL held are forwarded rather than
|
|
107
|
+
* silently dropped.
|
|
108
|
+
*/
|
|
109
|
+
export interface SurrogateQuery {
|
|
110
|
+
intent?: string
|
|
111
|
+
next?: string
|
|
112
|
+
method?: string
|
|
113
|
+
}
|
package/src/utils/trusted.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
|
|
2
2
|
import type { Context, Config } from './types.js'
|
|
3
3
|
import type { TrustedRecord } from '../types.js'
|
|
4
|
-
import type { Resource } from '@owlmeans/resource'
|
|
5
|
-
import {
|
|
4
|
+
import type { Criteria, Resource } from '@owlmeans/resource'
|
|
5
|
+
import { keyHelper, makeKeyPairModel } from '@owlmeans/basic-keys'
|
|
6
6
|
|
|
7
7
|
export const trust = async <C extends Config, T extends Context<C>>(context: T, resource: string, userName: string, field: string = 'name') => {
|
|
8
|
-
const
|
|
8
|
+
const where: Criteria<TrustedRecord> = { [field]: userName }
|
|
9
|
+
const trustedUser = await context.resource<Resource<TrustedRecord>>(resource).load(where)
|
|
9
10
|
if (trustedUser == null) {
|
|
10
11
|
throw new SyntaxError(`Auth service trusted entity is not provided: ${userName}`)
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// @TODO credential can really be undefined - we need to process it properly
|
|
14
|
-
const keyPair = trustedUser.secret != null ? makeKeyPairModel(trustedUser.secret) : fromPubKey(trustedUser.credential!)
|
|
15
|
+
const keyPair = trustedUser.secret != null ? makeKeyPairModel(trustedUser.secret) : keyHelper.fromPubKey(trustedUser.credential!)
|
|
15
16
|
|
|
16
17
|
return { user: trustedUser, key: keyPair }
|
|
17
18
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AuthenPayloadError, AuthroizationType } from '@owlmeans/auth'
|
|
3
|
+
import {
|
|
4
|
+
BED255_NONCE_HEADER, BED255_SIG_TTL, BED255_TIME_HEADER, makeBasicEd25519Guard,
|
|
5
|
+
makeResourceSignedRequestReplayStore
|
|
6
|
+
} from '@owlmeans/auth-common'
|
|
7
|
+
import type { BasicEd25519Guard, SignedRequestReplayResource } from '@owlmeans/auth-common'
|
|
8
|
+
import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint'
|
|
9
|
+
import type { Auth } from '@owlmeans/auth'
|
|
10
|
+
import { RecordExists } from '@owlmeans/resource'
|
|
11
|
+
import { makeTrustFixture, TRUSTED_ALIAS } from './context.js'
|
|
12
|
+
|
|
13
|
+
const timeKey = BED255_TIME_HEADER.toLowerCase()
|
|
14
|
+
const nonceKey = BED255_NONCE_HEADER.toLowerCase()
|
|
15
|
+
|
|
16
|
+
const request = async (timestamp: string, nonce: string) => {
|
|
17
|
+
const fixture = makeTrustFixture({ withSecret: true, name: 'auth-common-tests' })
|
|
18
|
+
const body = { action: 'publish' }
|
|
19
|
+
const signature = await fixture.authKey.sign({
|
|
20
|
+
body,
|
|
21
|
+
headers: { [timeKey]: timestamp, [nonceKey]: nonce }
|
|
22
|
+
})
|
|
23
|
+
const req = {
|
|
24
|
+
alias: 'signed', body, headers: {
|
|
25
|
+
[timeKey]: timestamp,
|
|
26
|
+
[nonceKey]: nonce,
|
|
27
|
+
authorization: `${AuthroizationType.Ed25519BasicSignature.toUpperCase()} `
|
|
28
|
+
+ `Credential=${fixture.authRecord.id} Signature=${signature}`,
|
|
29
|
+
}, params: {}, query: {}, path: '/signed'
|
|
30
|
+
} satisfies AbstractRequest
|
|
31
|
+
const guard = makeBasicEd25519Guard(TRUSTED_ALIAS)
|
|
32
|
+
fixture.ctx.registerService(guard)
|
|
33
|
+
|
|
34
|
+
return { fixture, guard, req }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handle = async (
|
|
38
|
+
guard: BasicEd25519Guard, req: AbstractRequest
|
|
39
|
+
): Promise<Auth | undefined> => {
|
|
40
|
+
let value: Auth | undefined
|
|
41
|
+
const response: AbstractResponse<Auth> = {
|
|
42
|
+
resolve: resolved => { value = resolved },
|
|
43
|
+
reject: error => { throw error }
|
|
44
|
+
}
|
|
45
|
+
await guard.handle(req, response)
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('@owlmeans/auth-common — signed request freshness and replay protection', () => {
|
|
50
|
+
test('accepts a canonical current timestamp once and rejects the replay by default', async () => {
|
|
51
|
+
const signed = await request(new Date().toISOString(), 'one-use-nonce')
|
|
52
|
+
|
|
53
|
+
expect((await handle(signed.guard, signed.req))?.userId).toBe(signed.fixture.authRecord.id)
|
|
54
|
+
await expect(handle(signed.guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('rejects parseable but non-canonical timestamps', async () => {
|
|
58
|
+
const signed = await request(new Date().toUTCString(), 'non-canonical')
|
|
59
|
+
|
|
60
|
+
await expect(handle(signed.guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('rejects timestamps outside the symmetric 60-second window', async () => {
|
|
64
|
+
const past = await request(new Date(Date.now() - BED255_SIG_TTL - 1_000).toISOString(), 'past')
|
|
65
|
+
const future = await request(new Date(Date.now() + BED255_SIG_TTL + 1_000).toISOString(), 'future')
|
|
66
|
+
|
|
67
|
+
await expect(handle(past.guard, past.req)).rejects.toBeInstanceOf(AuthenPayloadError)
|
|
68
|
+
await expect(handle(future.guard, future.req)).rejects.toBeInstanceOf(AuthenPayloadError)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('rejects authentication when an injected replay store fails', async () => {
|
|
72
|
+
const signed = await request(new Date().toISOString(), 'store-failure')
|
|
73
|
+
const guard = makeBasicEd25519Guard(TRUSTED_ALIAS, {
|
|
74
|
+
replay: { claim: async () => { throw new Error('store unavailable') } }
|
|
75
|
+
})
|
|
76
|
+
signed.fixture.ctx.registerService(guard)
|
|
77
|
+
|
|
78
|
+
await expect(handle(guard, signed.req)).rejects.toBeInstanceOf(AuthenPayloadError)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('adapts a create-once resource and forwards the absolute signed expiry', async () => {
|
|
82
|
+
let claimed = false
|
|
83
|
+
let observedExpiry: Date | undefined
|
|
84
|
+
const resource = {
|
|
85
|
+
create: async (record: { id?: string }, opts?: { ttl?: number | Date }) => {
|
|
86
|
+
if (claimed) throw new RecordExists(record.id ?? '')
|
|
87
|
+
claimed = true
|
|
88
|
+
observedExpiry = opts?.ttl as Date
|
|
89
|
+
return record
|
|
90
|
+
}
|
|
91
|
+
} as SignedRequestReplayResource
|
|
92
|
+
const replay = makeResourceSignedRequestReplayStore(resource)
|
|
93
|
+
const expiresAt = new Date(Date.now() + BED255_SIG_TTL)
|
|
94
|
+
|
|
95
|
+
expect(await replay.claim('service:nonce', expiresAt)).toBe(true)
|
|
96
|
+
expect(observedExpiry).toBe(expiresAt)
|
|
97
|
+
expect(await replay.claim('service:nonce', expiresAt)).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
})
|
package/tests/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppType,
|
|
1
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
2
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
3
|
import { makeMemoryTrustedResource, makeFixtureKeyPair } from '@owlmeans/test-auth'
|
|
4
4
|
import type { TrustedRecord } from '@owlmeans/auth-common'
|
|
@@ -38,7 +38,6 @@ export const makeTrustFixture = (
|
|
|
38
38
|
const ctx = makeBasicContext<BasicConfig>({
|
|
39
39
|
ready: false,
|
|
40
40
|
service: 'auth-common-tests',
|
|
41
|
-
layer: Layer.Service,
|
|
42
41
|
type: AppType.Backend,
|
|
43
42
|
})
|
|
44
43
|
|
package/tests/dispatcher.spec.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { authProtocols } from '@owlmeans/auth-common'
|
|
3
3
|
import { DISPATCHER, DISPATCHER_AUTHEN } from '@owlmeans/auth'
|
|
4
4
|
import { DISPATCHER_PATH } from '../src/consts.js'
|
|
5
5
|
|
|
6
|
-
// The DISPATCHER
|
|
7
|
-
//
|
|
6
|
+
// The DISPATCHER entrypoint intentionally declares service: DISPATCHER so that
|
|
7
|
+
// url() produces an absolute URL (pointing to the auth service).
|
|
8
8
|
// The fix for the React Router 404 ("/https:/vib.owl.flat/dispatcher") lives
|
|
9
9
|
// in @owlmeans/client's navigator.navigate() — when the URL is absolute it
|
|
10
10
|
// does a browser redirect (globalThis.location.href) instead of React Router navigate().
|
|
11
11
|
|
|
12
|
-
describe('@owlmeans/auth-common — DISPATCHER
|
|
13
|
-
const dispatcher =
|
|
12
|
+
describe('@owlmeans/auth-common — DISPATCHER protocol declaration', () => {
|
|
13
|
+
const dispatcher = authProtocols.dispatcher
|
|
14
14
|
|
|
15
|
-
test('DISPATCHER
|
|
16
|
-
expect(dispatcher).
|
|
15
|
+
test('DISPATCHER protocol is present in the authentication tree', () => {
|
|
16
|
+
expect(dispatcher.alias).toBe(DISPATCHER)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('DISPATCHER route has an explicit service override (cross-service)', () => {
|
|
20
20
|
// service: DISPATCHER is intentional — the dispatcher belongs to the auth service.
|
|
21
21
|
// The navigator in @owlmeans/client handles the resulting absolute URL correctly.
|
|
22
|
-
expect(dispatcher
|
|
22
|
+
expect(dispatcher.route.route.service).toBe(DISPATCHER)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
test('DISPATCHER route path is DISPATCHER_PATH (/dispatcher)', () => {
|
|
26
|
-
expect(dispatcher
|
|
26
|
+
expect(dispatcher.route.route.path).toBe(DISPATCHER_PATH)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
test('DISPATCHER
|
|
30
|
-
expect(dispatcher
|
|
29
|
+
test('DISPATCHER entrypoint is sticky (always registered in every app router)', () => {
|
|
30
|
+
expect(dispatcher.sticky).toBe(true)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
test('DISPATCHER_AUTHEN backend
|
|
34
|
-
const dispatcherAuthen =
|
|
35
|
-
expect(dispatcherAuthen).
|
|
36
|
-
expect(dispatcherAuthen
|
|
33
|
+
test('DISPATCHER_AUTHEN backend entrypoint is distinct', () => {
|
|
34
|
+
const dispatcherAuthen = authProtocols.dispatcherAuthenticate
|
|
35
|
+
expect(dispatcherAuthen.alias).toBe(DISPATCHER_AUTHEN)
|
|
36
|
+
expect(dispatcherAuthen.sticky).toBeFalsy()
|
|
37
37
|
})
|
|
38
38
|
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AppType, createService, makeBasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
4
|
+
import { AuthenFailed, AuthRole } from '@owlmeans/auth'
|
|
5
|
+
import type { Auth } from '@owlmeans/auth'
|
|
6
|
+
import type { AbstractRequest, ResolvedEntity } from '@owlmeans/entrypoint'
|
|
7
|
+
import { makeEntityScope } from '../src/entity.js'
|
|
8
|
+
import { ENTITY_RESOLVER } from '../src/consts.js'
|
|
9
|
+
import type { EntityResolverService, OrgEntityRef } from '../src/types.js'
|
|
10
|
+
|
|
11
|
+
const ACME: OrgEntityRef = { id: '0123456789abcdef01234567', slug: 'acme', formerSlugs: ['acme-old'], iamKey: 'acme-key' }
|
|
12
|
+
|
|
13
|
+
/** A context with an in-memory registry of one organization, or none at all. */
|
|
14
|
+
const contextWith = async (registry?: OrgEntityRef[]) => {
|
|
15
|
+
const ctx = makeBasicContext<BasicConfig>({ ready: false, service: 'entity-tests', type: AppType.Backend })
|
|
16
|
+
const calls: string[] = []
|
|
17
|
+
if (registry != null) {
|
|
18
|
+
ctx.registerService(createService<EntityResolverService>(ENTITY_RESOLVER, {
|
|
19
|
+
resolve: async value => {
|
|
20
|
+
calls.push(value)
|
|
21
|
+
return registry.find(entity => entity.id === value || entity.slug === value
|
|
22
|
+
|| entity.formerSlugs?.includes(value) || entity.iamKey === value) ?? null
|
|
23
|
+
},
|
|
24
|
+
byId: async id => registry.find(entity => entity.id === id) ?? null,
|
|
25
|
+
mintSlug: async () => 'minted',
|
|
26
|
+
rename: async () => { throw new Error('not used') },
|
|
27
|
+
mintName: async () => { throw new Error('not used') },
|
|
28
|
+
}))
|
|
29
|
+
}
|
|
30
|
+
ctx.configure()
|
|
31
|
+
await ctx.init()
|
|
32
|
+
|
|
33
|
+
return { ctx: ctx as BasicContext<BasicConfig>, calls }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const requestFor = (entitySlug?: string, entity?: ResolvedEntity): AbstractRequest => ({
|
|
37
|
+
alias: 'any', path: '/', params: {}, query: {}, headers: {},
|
|
38
|
+
auth: { token: 't', type: 'oidc', userId: 'u', role: AuthRole.User, scopes: [], isUser: true,
|
|
39
|
+
createdAt: new Date(), ...(entitySlug != null ? { entitySlug } : {}) } as Auth,
|
|
40
|
+
...(entity != null ? { entity } : {}),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const attached: ResolvedEntity = { id: 'iam-key-tenant', slug: 'tenant', iamKey: 'iam-key-tenant' }
|
|
44
|
+
|
|
45
|
+
describe('attachEntity — an entity the guard attached', () => {
|
|
46
|
+
test('is kept when its slug is the token one, with no resolver registered', async () => {
|
|
47
|
+
const { ctx } = await contextWith()
|
|
48
|
+
const request = requestFor('tenant', attached)
|
|
49
|
+
|
|
50
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toEqual(attached)
|
|
51
|
+
expect(request.entity).toEqual(attached)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('is kept without asking a registered resolver', async () => {
|
|
55
|
+
const { ctx, calls } = await contextWith([ACME])
|
|
56
|
+
const request = requestFor('tenant', attached)
|
|
57
|
+
|
|
58
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toEqual(attached)
|
|
59
|
+
expect(calls).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('with another slug falls through to the resolver', async () => {
|
|
63
|
+
const { ctx, calls } = await contextWith([ACME])
|
|
64
|
+
const request = requestFor('acme', attached)
|
|
65
|
+
|
|
66
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toEqual(ACME)
|
|
67
|
+
expect(request.entity).toEqual(ACME)
|
|
68
|
+
expect(calls).toEqual(['acme'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('with another slug and no resolver is dropped, never trusted', async () => {
|
|
72
|
+
const { ctx } = await contextWith()
|
|
73
|
+
const request = requestFor('acme', attached)
|
|
74
|
+
|
|
75
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toBeUndefined()
|
|
76
|
+
expect(request.entity).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('on a token naming no organization is dropped', async () => {
|
|
80
|
+
const { ctx } = await contextWith()
|
|
81
|
+
const request = requestFor(undefined, attached)
|
|
82
|
+
|
|
83
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toBeUndefined()
|
|
84
|
+
expect(request.entity).toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('attachEntity — the resolver path', () => {
|
|
89
|
+
test('resolves and canonicalizes a retired slug', async () => {
|
|
90
|
+
const { ctx } = await contextWith([ACME])
|
|
91
|
+
const request = requestFor('acme-old')
|
|
92
|
+
|
|
93
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toEqual(ACME)
|
|
94
|
+
expect(request.entity).toEqual(ACME)
|
|
95
|
+
expect(request.auth?.entitySlug).toBe('acme')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('refuses a token naming an organization the registry does not know', async () => {
|
|
99
|
+
const { ctx } = await contextWith([ACME])
|
|
100
|
+
|
|
101
|
+
await expect(makeEntityScope(requestFor('nobody')).attachEntity(ctx)).rejects.toBeInstanceOf(AuthenFailed)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('is a no-op with no resolver and nothing attached', async () => {
|
|
105
|
+
const { ctx } = await contextWith()
|
|
106
|
+
const request = requestFor('acme')
|
|
107
|
+
|
|
108
|
+
expect(await makeEntityScope(request).attachEntity(ctx)).toBeUndefined()
|
|
109
|
+
expect(request.entity).toBeUndefined()
|
|
110
|
+
expect(request.auth?.entitySlug).toBe('acme')
|
|
111
|
+
})
|
|
112
|
+
})
|
package/src/modules.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
AUTHEN, AUTHEN_AUTHEN, AUTHEN_INIT, AUTHEN_RELY, AllowanceRequestSchema, AuthCredentialsSchema,
|
|
4
|
-
AuthTokenSchema, CAUTHEN, CAUTHEN_AUTHEN, CAUTHEN_AUTHEN_DEFAULT, CAUTHEN_AUTHEN_TYPED, DISPATCHER,
|
|
5
|
-
DISPATCHER_AUTHEN, DISPATCHER_SURROGATE, OptionalAuthTokenSchema, CAUTHEN_FLOW_ENTER
|
|
6
|
-
} from '@owlmeans/auth'
|
|
7
|
-
// import { AppType } from '@owlmeans/context'
|
|
8
|
-
import { body, filter, entrypoint, query } from '@owlmeans/entrypoint'
|
|
9
|
-
import { route, RouteMethod, frontend, backend, socket } from '@owlmeans/route'
|
|
10
|
-
import { DISPATCHER_PATH, SURROGATE_PATH, WEB_API, authApi } from './consts.js'
|
|
11
|
-
import { SurrogateQuerySchema } from './schemas.js'
|
|
12
|
-
|
|
13
|
-
export const modules = [
|
|
14
|
-
entrypoint(route(AUTHEN, '/authentication', backend())),
|
|
15
|
-
entrypoint(route(AUTHEN_INIT, '/init', backend(AUTHEN, RouteMethod.POST)), filter(body(AllowanceRequestSchema))),
|
|
16
|
-
entrypoint(route(AUTHEN_AUTHEN, '/authenticate', backend(AUTHEN, RouteMethod.POST)), filter(body(AuthCredentialsSchema))),
|
|
17
|
-
entrypoint(route(AUTHEN_RELY, '/rely', socket(AUTHEN)), filter(query(OptionalAuthTokenSchema))),
|
|
18
|
-
entrypoint(route(CAUTHEN, '/authentication', frontend())),
|
|
19
|
-
entrypoint(route(CAUTHEN_AUTHEN, '/login', frontend(CAUTHEN))),
|
|
20
|
-
entrypoint(route(CAUTHEN_AUTHEN_DEFAULT, '/', frontend(CAUTHEN_AUTHEN, true))),
|
|
21
|
-
entrypoint(route(CAUTHEN_AUTHEN_TYPED, '/:type', frontend(CAUTHEN_AUTHEN))),
|
|
22
|
-
// This is a helper route that is used by service providers to redirect to an external authentication service.
|
|
23
|
-
entrypoint(route(CAUTHEN_FLOW_ENTER, '/', frontend())),
|
|
24
|
-
// This is a helper route that is useb by service providres to process authentication provisioning via redirect.
|
|
25
|
-
// Also it can be default starting point to be redirected to an external identity provider.
|
|
26
|
-
entrypoint(
|
|
27
|
-
route(DISPATCHER, DISPATCHER_PATH, frontend({ service: DISPATCHER })),
|
|
28
|
-
// This module is sticky - it means it's always attach to client router.
|
|
29
|
-
// It's required here cause every web app needs dispatcher route to authorize
|
|
30
|
-
// rediected users.
|
|
31
|
-
filter(query(AuthTokenSchema), { sticky: true })
|
|
32
|
-
),
|
|
33
|
-
// The login window an embedded application opens one level up.
|
|
34
|
-
//
|
|
35
|
-
// Top level and with no parent, so it renders outside every application layout — a popup must
|
|
36
|
-
// never show the application, with its navigation, inside itself. Sticky for the same reason the
|
|
37
|
-
// dispatcher is: it must attach to the client router regardless of service selection. It carries
|
|
38
|
-
// no `service`, unlike the dispatcher, because nothing server-side ever addresses it — the
|
|
39
|
-
// provider's callback goes to the dispatcher. And no guard: it is where a signed-out user lands.
|
|
40
|
-
entrypoint(
|
|
41
|
-
route(DISPATCHER_SURROGATE, SURROGATE_PATH, frontend()),
|
|
42
|
-
filter(query(SurrogateQuerySchema), { sticky: true })
|
|
43
|
-
),
|
|
44
|
-
// This is a helper route that representes a API endpoint of service provider that wants to authenticate
|
|
45
|
-
// a user with OwlMeans server-auth library.
|
|
46
|
-
entrypoint(route(DISPATCHER_AUTHEN, '/authenticate', backend(null, RouteMethod.POST)), filter(body(AuthTokenSchema))),
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
export const managerModules = [
|
|
50
|
-
entrypoint(route(authApi.profile.base, '/profile', backend({ service: WEB_API }))),
|
|
51
|
-
entrypoint(route(
|
|
52
|
-
authApi.profile.toEntityId, '/to-entity-id',
|
|
53
|
-
backend(authApi.profile.base, RouteMethod.POST)
|
|
54
|
-
)),
|
|
55
|
-
entrypoint(route(authApi.auth.base, '/auth', backend({ service: WEB_API }))),
|
|
56
|
-
entrypoint(route(
|
|
57
|
-
authApi.auth.delegate, '/delegate',
|
|
58
|
-
backend(authApi.auth.base, RouteMethod.POST)
|
|
59
|
-
)),
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
// const skipModules = [DISPATCHER]
|
|
63
|
-
// export const authBackendModules = modules.filter(
|
|
64
|
-
// module => !skipModules.includes(module.alias) && module.route.route.type === AppType.Backend
|
|
65
|
-
// ).map(module => module.alias)
|