@owlmeans/server-auth 0.1.2 → 0.1.4
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 +33 -454
- package/build/actions/service.d.ts +2 -2
- package/build/actions/service.d.ts.map +1 -1
- package/build/actions/service.js.map +1 -1
- package/build/manager/actions/manager.d.ts +4 -4
- package/build/manager/actions/manager.d.ts.map +1 -1
- package/build/manager/actions/manager.js.map +1 -1
- package/build/manager/exports.d.ts +4 -4
- package/build/manager/exports.d.ts.map +1 -1
- package/build/manager/exports.js +2 -2
- package/build/manager/exports.js.map +1 -1
- package/build/manager/main.js +1 -1
- package/build/manager/main.js.map +1 -1
- package/build/manager/model.js +2 -2
- package/build/manager/model.js.map +1 -1
- package/build/manager/modules.d.ts +1 -1
- package/build/manager/modules.d.ts.map +1 -1
- package/build/manager/modules.js +3 -3
- package/build/manager/modules.js.map +1 -1
- package/build/manager/plugins/re-captcha.js +3 -3
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/types.d.ts +1 -1
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/manager/plugins/utils.d.ts +1 -1
- package/build/manager/plugins/utils.d.ts.map +1 -1
- package/build/manager/plugins/utils.js +2 -2
- package/build/manager/plugins/utils.js.map +1 -1
- package/build/manager/types.d.ts +1 -1
- package/build/manager/types.d.ts.map +1 -1
- package/build/modules.d.ts +2 -2
- package/build/modules.d.ts.map +1 -1
- package/build/modules.js +2 -2
- package/build/modules.js.map +1 -1
- package/build/types.d.ts +1 -1
- package/build/types.d.ts.map +1 -1
- package/package.json +30 -26
- package/src/actions/service.ts +2 -2
- package/src/manager/actions/manager.ts +4 -4
- package/src/manager/exports.ts +4 -4
- package/src/manager/main.ts +1 -1
- package/src/manager/model.ts +2 -2
- package/src/manager/modules.ts +3 -3
- package/src/manager/plugins/re-captcha.ts +4 -4
- package/src/manager/plugins/types.ts +1 -1
- package/src/manager/plugins/utils.ts +2 -2
- package/src/manager/rely.ts +1 -1
- package/src/manager/types.ts +1 -1
- package/src/modules.ts +5 -5
- package/src/service.ts +1 -1
- package/src/types.ts +1 -1
- package/tests/context.ts +59 -0
- package/tests/service.spec.ts +174 -0
- package/tests/tsconfig.json +12 -0
- package/tsconfig.json +6 -5
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { makeTestContext } from './context.js'
|
|
3
|
+
import { makeFixtureKeyPair } from '@owlmeans/test-auth'
|
|
4
|
+
import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
|
|
5
|
+
import { AuthenFailed, AuthroizationType, AuthRole } from '@owlmeans/auth'
|
|
6
|
+
import type { AuthCredentials, Auth } from '@owlmeans/auth'
|
|
7
|
+
import { DEFAULT_ALIAS } from '../src/consts.js'
|
|
8
|
+
import type { AuthService } from '../src/types.js'
|
|
9
|
+
|
|
10
|
+
const initContext = async () => {
|
|
11
|
+
const { context, authServiceKP, appKP, authServiceRecord } = makeTestContext()
|
|
12
|
+
context.configure()
|
|
13
|
+
await context.init()
|
|
14
|
+
return { context, authServiceKP, appKP, authServiceRecord }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('@owlmeans/server-auth — authenticate (mode 2: external auth service credentials)', () => {
|
|
18
|
+
test('exchanges valid signed intermediate credentials into a bearer token', async () => {
|
|
19
|
+
const { context, authServiceKP, authServiceRecord } = await initContext()
|
|
20
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
21
|
+
|
|
22
|
+
// Build intermediate credentials as the external auth service would produce
|
|
23
|
+
const credentials: AuthCredentials = {
|
|
24
|
+
type: 'ed25519-basic-token',
|
|
25
|
+
challenge: 'unique-nonce-' + Date.now(),
|
|
26
|
+
credential: authServiceRecord.id, // must match trustedUser.id
|
|
27
|
+
role: AuthRole.User,
|
|
28
|
+
userId: 'user-123',
|
|
29
|
+
scopes: ['*'],
|
|
30
|
+
profileId: 'profile-abc',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Sign the credentials with the auth service's secret key
|
|
34
|
+
const signedToken = await makeEnvelopeModel<AuthCredentials>(AuthroizationType.Ed25519BasicToken)
|
|
35
|
+
.send(credentials, null).sign(authServiceKP, EnvelopeKind.Token)
|
|
36
|
+
|
|
37
|
+
const result = await authService.authenticate({ token: signedToken })
|
|
38
|
+
|
|
39
|
+
expect(result.token).toStartWith('ED25519-BASIC-TOKEN ')
|
|
40
|
+
// The token should be verifiable with the app's own key
|
|
41
|
+
const [, authorization] = result.token.split(' ')
|
|
42
|
+
expect(authorization.length).toBeGreaterThan(0)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('rejects credentials signed by an unknown key', async () => {
|
|
46
|
+
const { context } = await initContext()
|
|
47
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
48
|
+
|
|
49
|
+
const unknownKP = makeFixtureKeyPair('unknown-signer')
|
|
50
|
+
|
|
51
|
+
const credentials: AuthCredentials = {
|
|
52
|
+
type: 'ed25519-basic-token',
|
|
53
|
+
challenge: 'nonce-' + Date.now(),
|
|
54
|
+
credential: 'wrong-id',
|
|
55
|
+
role: AuthRole.User,
|
|
56
|
+
userId: 'user-456',
|
|
57
|
+
scopes: ['*'],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const signedToken = await makeEnvelopeModel<AuthCredentials>(AuthroizationType.Ed25519BasicToken)
|
|
61
|
+
.send(credentials, null).sign(unknownKP, EnvelopeKind.Token)
|
|
62
|
+
|
|
63
|
+
await expect(authService.authenticate({ token: signedToken })).rejects.toBeInstanceOf(AuthenFailed)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('rejects when credential does not match trusted user id (anti-impersonation)', async () => {
|
|
67
|
+
const { context, authServiceKP } = await initContext()
|
|
68
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
69
|
+
|
|
70
|
+
const credentials: AuthCredentials = {
|
|
71
|
+
type: 'ed25519-basic-token',
|
|
72
|
+
challenge: 'nonce-impersonate-' + Date.now(),
|
|
73
|
+
credential: 'wrong-service-user-id', // does not match authServiceRecord.id
|
|
74
|
+
role: AuthRole.User,
|
|
75
|
+
userId: 'user-789',
|
|
76
|
+
scopes: ['*'],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const signedToken = await makeEnvelopeModel<AuthCredentials>(AuthroizationType.Ed25519BasicToken)
|
|
80
|
+
.send(credentials, null).sign(authServiceKP, EnvelopeKind.Token)
|
|
81
|
+
|
|
82
|
+
await expect(authService.authenticate({ token: signedToken })).rejects.toBeInstanceOf(AuthenFailed)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('rejects replay of already-used challenge (anti-replay cache)', async () => {
|
|
86
|
+
const { context, authServiceKP, authServiceRecord } = await initContext()
|
|
87
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
88
|
+
|
|
89
|
+
const challenge = 'replay-nonce-' + Date.now()
|
|
90
|
+
const credentials: AuthCredentials = {
|
|
91
|
+
type: 'ed25519-basic-token',
|
|
92
|
+
challenge,
|
|
93
|
+
credential: authServiceRecord.id,
|
|
94
|
+
role: AuthRole.User,
|
|
95
|
+
userId: 'user-replay',
|
|
96
|
+
scopes: ['*'],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const signedToken = await makeEnvelopeModel<AuthCredentials>(AuthroizationType.Ed25519BasicToken)
|
|
100
|
+
.send(credentials, null).sign(authServiceKP, EnvelopeKind.Token)
|
|
101
|
+
|
|
102
|
+
// First call succeeds
|
|
103
|
+
await authService.authenticate({ token: signedToken })
|
|
104
|
+
|
|
105
|
+
// Second call with same challenge should fail (replay)
|
|
106
|
+
await expect(authService.authenticate({ token: signedToken })).rejects.toBeInstanceOf(AuthenFailed)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('@owlmeans/server-auth — handle (mode 3: verify bearer token)', () => {
|
|
111
|
+
test('verifies a bearer token issued by authenticate()', async () => {
|
|
112
|
+
const { context, authServiceKP, authServiceRecord } = await initContext()
|
|
113
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
114
|
+
|
|
115
|
+
// First get a valid bearer via authenticate
|
|
116
|
+
const credentials: AuthCredentials = {
|
|
117
|
+
type: 'ed25519-basic-token',
|
|
118
|
+
challenge: 'handle-nonce-' + Date.now(),
|
|
119
|
+
credential: authServiceRecord.id,
|
|
120
|
+
role: AuthRole.User,
|
|
121
|
+
userId: 'user-handle',
|
|
122
|
+
scopes: ['*'],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const signedToken = await makeEnvelopeModel<AuthCredentials>(AuthroizationType.Ed25519BasicToken)
|
|
126
|
+
.send(credentials, null).sign(authServiceKP, EnvelopeKind.Token)
|
|
127
|
+
|
|
128
|
+
const { token: bearer } = await authService.authenticate({ token: signedToken })
|
|
129
|
+
|
|
130
|
+
// Now verify it via handle()
|
|
131
|
+
const auth: { value: Auth | null } = { value: null }
|
|
132
|
+
const req = { headers: { authorization: bearer } } as any
|
|
133
|
+
const res = { resolve: (v: Auth) => { auth.value = v } } as any
|
|
134
|
+
|
|
135
|
+
const handled = await authService.handle(req, res)
|
|
136
|
+
|
|
137
|
+
expect(handled).toBe(true)
|
|
138
|
+
expect(auth.value).not.toBeNull()
|
|
139
|
+
expect(auth.value!.userId).toBe('user-handle')
|
|
140
|
+
expect(auth.value!.scopes).toEqual(['*'])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('returns false for missing authorization header', async () => {
|
|
144
|
+
const { context } = await initContext()
|
|
145
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
146
|
+
|
|
147
|
+
const req = { headers: {} } as any
|
|
148
|
+
const res = { resolve: () => {} } as any
|
|
149
|
+
|
|
150
|
+
const handled = await authService.handle(req, res)
|
|
151
|
+
|
|
152
|
+
expect(handled).toBe(false)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('@owlmeans/server-auth — match', () => {
|
|
157
|
+
test('matches Ed25519BasicToken authorization header', async () => {
|
|
158
|
+
const { context } = await initContext()
|
|
159
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
160
|
+
|
|
161
|
+
const req = { headers: { authorization: 'ED25519-BASIC-TOKEN abc123' } } as any
|
|
162
|
+
const res = {} as any
|
|
163
|
+
expect(await authService.match(req, res)).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('does not match other authorization types', async () => {
|
|
167
|
+
const { context } = await initContext()
|
|
168
|
+
const authService = context.service<AuthService>(DEFAULT_ALIAS)
|
|
169
|
+
|
|
170
|
+
const req = { headers: { authorization: 'Bearer xyz' } } as any
|
|
171
|
+
const res = {} as any
|
|
172
|
+
expect(await authService.match(req, res)).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": [
|
|
3
|
-
"
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.node.json"
|
|
4
5
|
],
|
|
5
6
|
"compilerOptions": {
|
|
6
|
-
"rootDir": "./src/",
|
|
7
|
-
"outDir": "./build/"
|
|
8
|
-
"moduleResolution": "Bundler"
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
9
|
},
|
|
10
10
|
"exclude": [
|
|
11
11
|
"./dist/**/*",
|
|
12
12
|
"./build/**/*",
|
|
13
|
+
"./tests/**/*",
|
|
13
14
|
"./*.ts"
|
|
14
15
|
]
|
|
15
|
-
}
|
|
16
|
+
}
|