wabe 0.6.9 → 0.6.11
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 +156 -50
- package/bucket/b.txt +1 -0
- package/dev/index.ts +215 -0
- package/dist/authentication/Session.d.ts +4 -1
- package/dist/authentication/interface.d.ts +16 -0
- package/dist/cron/index.d.ts +0 -1
- package/dist/database/DatabaseController.d.ts +41 -13
- package/dist/database/interface.d.ts +1 -0
- package/dist/email/DevAdapter.d.ts +0 -1
- package/dist/email/interface.d.ts +1 -1
- package/dist/graphql/resolvers.d.ts +4 -2
- package/dist/hooks/index.d.ts +8 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +32144 -32058
- package/dist/schema/Schema.d.ts +2 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/utils/crypto.d.ts +7 -0
- package/dist/utils/helper.d.ts +5 -1
- package/generated/schema.graphql +22 -14
- package/generated/wabe.ts +4 -4
- package/package.json +23 -23
- package/src/authentication/OTP.test.ts +69 -0
- package/src/authentication/OTP.ts +64 -0
- package/src/authentication/Session.test.ts +629 -0
- package/src/authentication/Session.ts +493 -0
- package/src/authentication/defaultAuthentication.ts +209 -0
- package/src/authentication/index.ts +3 -0
- package/src/authentication/interface.ts +155 -0
- package/src/authentication/oauth/GitHub.test.ts +91 -0
- package/src/authentication/oauth/GitHub.ts +121 -0
- package/src/authentication/oauth/Google.test.ts +91 -0
- package/src/authentication/oauth/Google.ts +101 -0
- package/src/authentication/oauth/Oauth2Client.test.ts +219 -0
- package/src/authentication/oauth/Oauth2Client.ts +135 -0
- package/src/authentication/oauth/index.ts +2 -0
- package/src/authentication/oauth/utils.test.ts +33 -0
- package/src/authentication/oauth/utils.ts +27 -0
- package/src/authentication/providers/EmailOTP.test.ts +127 -0
- package/src/authentication/providers/EmailOTP.ts +84 -0
- package/src/authentication/providers/EmailPassword.test.ts +176 -0
- package/src/authentication/providers/EmailPassword.ts +116 -0
- package/src/authentication/providers/EmailPasswordSRP.test.ts +208 -0
- package/src/authentication/providers/EmailPasswordSRP.ts +179 -0
- package/src/authentication/providers/GitHub.ts +24 -0
- package/src/authentication/providers/Google.ts +24 -0
- package/src/authentication/providers/OAuth.test.ts +185 -0
- package/src/authentication/providers/OAuth.ts +106 -0
- package/src/authentication/providers/PhonePassword.test.ts +176 -0
- package/src/authentication/providers/PhonePassword.ts +115 -0
- package/src/authentication/providers/QRCodeOTP.test.ts +77 -0
- package/src/authentication/providers/QRCodeOTP.ts +58 -0
- package/src/authentication/providers/index.ts +6 -0
- package/src/authentication/resolvers/refreshResolver.test.ts +30 -0
- package/src/authentication/resolvers/refreshResolver.ts +19 -0
- package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
- package/src/authentication/resolvers/signInWithResolver.test.ts +293 -0
- package/src/authentication/resolvers/signInWithResolver.ts +92 -0
- package/src/authentication/resolvers/signOutResolver.test.ts +38 -0
- package/src/authentication/resolvers/signOutResolver.ts +18 -0
- package/src/authentication/resolvers/signUpWithResolver.test.ts +180 -0
- package/src/authentication/resolvers/signUpWithResolver.ts +65 -0
- package/src/authentication/resolvers/verifyChallenge.test.ts +133 -0
- package/src/authentication/resolvers/verifyChallenge.ts +62 -0
- package/src/authentication/roles.test.ts +49 -0
- package/src/authentication/roles.ts +40 -0
- package/src/authentication/utils.test.ts +97 -0
- package/src/authentication/utils.ts +39 -0
- package/src/cache/InMemoryCache.test.ts +62 -0
- package/src/cache/InMemoryCache.ts +45 -0
- package/src/cron/index.test.ts +17 -0
- package/src/cron/index.ts +43 -0
- package/src/database/DatabaseController.test.ts +613 -0
- package/src/database/DatabaseController.ts +1007 -0
- package/src/database/index.test.ts +1372 -0
- package/src/database/index.ts +9 -0
- package/src/database/interface.ts +302 -0
- package/src/email/DevAdapter.ts +7 -0
- package/src/email/EmailController.test.ts +29 -0
- package/src/email/EmailController.ts +13 -0
- package/src/email/index.ts +2 -0
- package/src/email/interface.ts +36 -0
- package/src/email/templates/sendOtpCode.ts +120 -0
- package/src/file/FileController.ts +28 -0
- package/src/file/FileDevAdapter.ts +51 -0
- package/src/file/hookDeleteFile.ts +25 -0
- package/src/file/hookReadFile.ts +66 -0
- package/src/file/hookUploadFile.ts +50 -0
- package/src/file/index.test.ts +932 -0
- package/src/file/index.ts +2 -0
- package/src/file/interface.ts +39 -0
- package/src/graphql/GraphQLSchema.test.ts +4408 -0
- package/src/graphql/GraphQLSchema.ts +880 -0
- package/src/graphql/index.ts +2 -0
- package/src/graphql/parseGraphqlSchema.ts +85 -0
- package/src/graphql/parser.test.ts +203 -0
- package/src/graphql/parser.ts +542 -0
- package/src/graphql/pointerAndRelationFunction.ts +191 -0
- package/src/graphql/resolvers.ts +442 -0
- package/src/graphql/tests/aggregation.test.ts +1115 -0
- package/src/graphql/tests/e2e.test.ts +590 -0
- package/src/graphql/tests/scalars.test.ts +250 -0
- package/src/graphql/types.ts +227 -0
- package/src/hooks/HookObject.test.ts +122 -0
- package/src/hooks/HookObject.ts +165 -0
- package/src/hooks/authentication.ts +67 -0
- package/src/hooks/createUser.test.ts +77 -0
- package/src/hooks/createUser.ts +10 -0
- package/src/hooks/defaultFields.test.ts +176 -0
- package/src/hooks/defaultFields.ts +32 -0
- package/src/hooks/deleteSession.test.ts +181 -0
- package/src/hooks/deleteSession.ts +20 -0
- package/src/hooks/hashFieldHook.test.ts +152 -0
- package/src/hooks/hashFieldHook.ts +89 -0
- package/src/hooks/index.test.ts +258 -0
- package/src/hooks/index.ts +414 -0
- package/src/hooks/permissions.test.ts +412 -0
- package/src/hooks/permissions.ts +93 -0
- package/src/hooks/protected.test.ts +551 -0
- package/src/hooks/protected.ts +60 -0
- package/src/hooks/searchableFields.test.ts +147 -0
- package/src/hooks/searchableFields.ts +86 -0
- package/src/hooks/session.test.ts +134 -0
- package/src/hooks/session.ts +76 -0
- package/src/hooks/setEmail.test.ts +216 -0
- package/src/hooks/setEmail.ts +33 -0
- package/src/hooks/setupAcl.test.ts +618 -0
- package/src/hooks/setupAcl.ts +25 -0
- package/src/index.ts +9 -0
- package/src/schema/Schema.test.ts +482 -0
- package/src/schema/Schema.ts +757 -0
- package/src/schema/defaultResolvers.ts +93 -0
- package/src/schema/index.ts +1 -0
- package/src/schema/resolvers/meResolver.test.ts +62 -0
- package/src/schema/resolvers/meResolver.ts +10 -0
- package/src/schema/resolvers/resetPassword.test.ts +341 -0
- package/src/schema/resolvers/resetPassword.ts +63 -0
- package/src/schema/resolvers/sendEmail.test.ts +118 -0
- package/src/schema/resolvers/sendEmail.ts +21 -0
- package/src/schema/resolvers/sendOtpCode.test.ts +141 -0
- package/src/schema/resolvers/sendOtpCode.ts +52 -0
- package/src/security.test.ts +3434 -0
- package/src/server/defaultSessionHandler.test.ts +62 -0
- package/src/server/defaultSessionHandler.ts +105 -0
- package/src/server/generateCodegen.ts +433 -0
- package/src/server/index.test.ts +532 -0
- package/src/server/index.ts +334 -0
- package/src/server/interface.ts +11 -0
- package/src/server/routes/authHandler.ts +169 -0
- package/src/server/routes/index.ts +39 -0
- package/src/utils/crypto.test.ts +41 -0
- package/src/utils/crypto.ts +105 -0
- package/src/utils/export.ts +11 -0
- package/src/utils/helper.ts +204 -0
- package/src/utils/index.test.ts +11 -0
- package/src/utils/index.ts +189 -0
- package/src/utils/preload.ts +8 -0
- package/src/utils/testHelper.ts +116 -0
- package/tsconfig.json +32 -0
- package/bunfig.toml +0 -4
- package/dist/ai/index.d.ts +0 -1
- package/dist/ai/interface.d.ts +0 -9
- /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { beforeAll, afterAll, describe, expect, it, beforeEach } from 'bun:test'
|
|
2
|
+
import { type DevWabeTypes, getAnonymousClient } from '../../utils/helper'
|
|
3
|
+
import type { Wabe } from '../../server'
|
|
4
|
+
import { gql } from 'graphql-request'
|
|
5
|
+
import { setupTests, closeTests } from '../../utils/testHelper'
|
|
6
|
+
|
|
7
|
+
describe('SignUpWith', () => {
|
|
8
|
+
let wabe: Wabe<DevWabeTypes>
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
const setup = await setupTests()
|
|
12
|
+
wabe = setup.wabe
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await wabe.controllers.database.clearDatabase()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await closeTests(wabe)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should throw an error if user already exist with emailPassword', async () => {
|
|
24
|
+
const anonymousClient = getAnonymousClient(wabe.config.port)
|
|
25
|
+
|
|
26
|
+
await anonymousClient.request<any>(
|
|
27
|
+
gql`
|
|
28
|
+
mutation signUpWith($input: SignUpWithInput!) {
|
|
29
|
+
signUpWith(input: $input) {
|
|
30
|
+
id
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
{
|
|
35
|
+
input: {
|
|
36
|
+
authentication: {
|
|
37
|
+
emailPassword: {
|
|
38
|
+
email: 'test@gmail.com',
|
|
39
|
+
password: 'password',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
anonymousClient.request<any>(
|
|
48
|
+
gql`
|
|
49
|
+
mutation signUpWith($input: SignUpWithInput!) {
|
|
50
|
+
signUpWith(input: $input) {
|
|
51
|
+
id
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`,
|
|
55
|
+
{
|
|
56
|
+
input: {
|
|
57
|
+
authentication: {
|
|
58
|
+
emailPassword: {
|
|
59
|
+
email: 'test@gmail.com',
|
|
60
|
+
password: 'password',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
),
|
|
66
|
+
).rejects.toThrow('Not authorized to create user')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should throw an error if the signUp is disabled', () => {
|
|
70
|
+
if (wabe.config) {
|
|
71
|
+
wabe.config.authentication = {
|
|
72
|
+
...wabe.config.authentication,
|
|
73
|
+
disableSignUp: true,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const anonymousClient = getAnonymousClient(wabe.config.port)
|
|
77
|
+
|
|
78
|
+
const userSchema = wabe.config.schema?.classes?.find((classItem) => classItem.name === 'User')
|
|
79
|
+
|
|
80
|
+
if (!userSchema) throw new Error('Failed to find user schema')
|
|
81
|
+
|
|
82
|
+
// @ts-expect-error
|
|
83
|
+
userSchema.permissions.create.requireAuthentication = true
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
anonymousClient.request<any>(
|
|
87
|
+
gql`
|
|
88
|
+
mutation signUpWith($input: SignUpWithInput!) {
|
|
89
|
+
signUpWith(input: $input) {
|
|
90
|
+
id
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`,
|
|
94
|
+
{
|
|
95
|
+
input: {
|
|
96
|
+
authentication: {
|
|
97
|
+
emailPassword: {
|
|
98
|
+
email: 'email@test.fr',
|
|
99
|
+
password: 'password',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
).rejects.toThrow('SignUp is disabled')
|
|
106
|
+
|
|
107
|
+
if (wabe.config) {
|
|
108
|
+
wabe.config.authentication = {
|
|
109
|
+
...wabe.config.authentication,
|
|
110
|
+
disableSignUp: false,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should block the signUpWith if the user creation is blocked for anonymous (the creation is done with root to avoid ACL issues)', () => {
|
|
116
|
+
const anonymousClient = getAnonymousClient(wabe.config.port)
|
|
117
|
+
|
|
118
|
+
const userSchema = wabe.config.schema?.classes?.find((classItem) => classItem.name === 'User')
|
|
119
|
+
|
|
120
|
+
if (!userSchema) throw new Error('Failed to find user schema')
|
|
121
|
+
|
|
122
|
+
// @ts-expect-error
|
|
123
|
+
userSchema.permissions.create.requireAuthentication = true
|
|
124
|
+
|
|
125
|
+
expect(
|
|
126
|
+
anonymousClient.request<any>(
|
|
127
|
+
gql`
|
|
128
|
+
mutation signUpWith($input: SignUpWithInput!) {
|
|
129
|
+
signUpWith(input: $input) {
|
|
130
|
+
id
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`,
|
|
134
|
+
{
|
|
135
|
+
input: {
|
|
136
|
+
authentication: {
|
|
137
|
+
emailPassword: {
|
|
138
|
+
email: 'email@test.fr',
|
|
139
|
+
password: 'password',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
),
|
|
145
|
+
).rejects.toThrow('Permission denied to create class User')
|
|
146
|
+
|
|
147
|
+
// @ts-expect-error
|
|
148
|
+
userSchema.permissions.create.requireAuthentication = false
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should signUpWith email and password when the user not exist', async () => {
|
|
152
|
+
const anonymousClient = getAnonymousClient(wabe.config.port)
|
|
153
|
+
|
|
154
|
+
const res = await anonymousClient.request<any>(
|
|
155
|
+
gql`
|
|
156
|
+
mutation signUpWith($input: SignUpWithInput!) {
|
|
157
|
+
signUpWith(input: $input) {
|
|
158
|
+
id
|
|
159
|
+
accessToken
|
|
160
|
+
refreshToken
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
{
|
|
165
|
+
input: {
|
|
166
|
+
authentication: {
|
|
167
|
+
emailPassword: {
|
|
168
|
+
email: 'test@gmail.com',
|
|
169
|
+
password: 'password',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expect(res.signUpWith.id).toEqual(expect.any(String))
|
|
177
|
+
expect(res.signUpWith.accessToken).toEqual(expect.any(String))
|
|
178
|
+
expect(res.signUpWith.refreshToken).toEqual(expect.any(String))
|
|
179
|
+
})
|
|
180
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SignUpWithInput } from '../../../generated/wabe'
|
|
2
|
+
import type { WabeContext } from '../../server/interface'
|
|
3
|
+
import { Session } from '../Session'
|
|
4
|
+
|
|
5
|
+
// 0 - Get the authentication method
|
|
6
|
+
// 1 - We check if the signUp is possible (call onSign)
|
|
7
|
+
// 2 - We create the user
|
|
8
|
+
// 3 - We create session
|
|
9
|
+
export const signUpWithResolver = async (
|
|
10
|
+
_: any,
|
|
11
|
+
{
|
|
12
|
+
input,
|
|
13
|
+
}: {
|
|
14
|
+
input: SignUpWithInput
|
|
15
|
+
},
|
|
16
|
+
context: WabeContext<any>,
|
|
17
|
+
) => {
|
|
18
|
+
if (context.wabe.config.authentication?.disableSignUp) throw new Error('SignUp is disabled')
|
|
19
|
+
|
|
20
|
+
// Create object call the provider signUp
|
|
21
|
+
const res = await context.wabe.controllers.database.createObject({
|
|
22
|
+
className: 'User',
|
|
23
|
+
data: {
|
|
24
|
+
authentication: input.authentication,
|
|
25
|
+
},
|
|
26
|
+
context,
|
|
27
|
+
select: { id: true },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const createdUserId = res?.id
|
|
31
|
+
|
|
32
|
+
const session = new Session()
|
|
33
|
+
|
|
34
|
+
if (!createdUserId) throw new Error('User not created')
|
|
35
|
+
|
|
36
|
+
const { accessToken, refreshToken, csrfToken } = await session.create(createdUserId, context)
|
|
37
|
+
|
|
38
|
+
if (context.wabe.config.authentication?.session?.cookieSession) {
|
|
39
|
+
context.response?.setCookie('refreshToken', refreshToken, {
|
|
40
|
+
httpOnly: true,
|
|
41
|
+
path: '/',
|
|
42
|
+
sameSite: 'Strict',
|
|
43
|
+
secure: true,
|
|
44
|
+
expires: session.getRefreshTokenExpireAt(context.wabe.config),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
context.response?.setCookie('accessToken', accessToken, {
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
path: '/',
|
|
50
|
+
sameSite: 'Strict',
|
|
51
|
+
secure: true,
|
|
52
|
+
expires: session.getAccessTokenExpireAt(context.wabe.config),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
context.response?.setCookie('csrfToken', csrfToken, {
|
|
56
|
+
httpOnly: true,
|
|
57
|
+
path: '/',
|
|
58
|
+
sameSite: 'Strict',
|
|
59
|
+
secure: true,
|
|
60
|
+
expires: session.getAccessTokenExpireAt(context.wabe.config),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { accessToken, refreshToken, id: createdUserId }
|
|
65
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, mock, spyOn } from 'bun:test'
|
|
2
|
+
import { verifyChallengeResolver } from './verifyChallenge'
|
|
3
|
+
import type { WabeContext } from '../../server/interface'
|
|
4
|
+
import { Session } from '../Session'
|
|
5
|
+
|
|
6
|
+
describe('verifyChallenge', () => {
|
|
7
|
+
const mockOnVerifyChallenge = mock(() => Promise.resolve(true))
|
|
8
|
+
|
|
9
|
+
const context: WabeContext<any> = {
|
|
10
|
+
sessionId: 'sessionId',
|
|
11
|
+
user: {
|
|
12
|
+
id: 'userId',
|
|
13
|
+
} as any,
|
|
14
|
+
wabe: {
|
|
15
|
+
config: {
|
|
16
|
+
authentication: {
|
|
17
|
+
customAuthenticationMethods: [
|
|
18
|
+
{
|
|
19
|
+
name: 'fakeOtp',
|
|
20
|
+
input: {
|
|
21
|
+
code: {
|
|
22
|
+
type: 'String',
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
provider: {
|
|
27
|
+
onVerifyChallenge: mockOnVerifyChallenge,
|
|
28
|
+
onSendChallenge: () => Promise.resolve(),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} as any
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockOnVerifyChallenge.mockClear()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should throw an error if no one factor is provided', () => {
|
|
42
|
+
expect(
|
|
43
|
+
verifyChallengeResolver(
|
|
44
|
+
undefined,
|
|
45
|
+
{
|
|
46
|
+
input: {},
|
|
47
|
+
},
|
|
48
|
+
context,
|
|
49
|
+
),
|
|
50
|
+
).rejects.toThrow('One factor is required')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should throw an error if more than one factor is provided', () => {
|
|
54
|
+
expect(
|
|
55
|
+
verifyChallengeResolver(
|
|
56
|
+
undefined,
|
|
57
|
+
{
|
|
58
|
+
input: {
|
|
59
|
+
secondFA: {
|
|
60
|
+
// @ts-expect-error
|
|
61
|
+
factor1: {},
|
|
62
|
+
factor2: {},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
context,
|
|
67
|
+
),
|
|
68
|
+
).rejects.toThrow('Only one factor is allowed')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should throw an error if the onVerifyChallenge failed', () => {
|
|
72
|
+
mockOnVerifyChallenge.mockResolvedValue(false as never)
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
verifyChallengeResolver(
|
|
76
|
+
undefined,
|
|
77
|
+
{
|
|
78
|
+
input: {
|
|
79
|
+
secondFA: {
|
|
80
|
+
// @ts-expect-error
|
|
81
|
+
fakeOtp: {
|
|
82
|
+
code: '123456',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
context,
|
|
88
|
+
),
|
|
89
|
+
).rejects.toThrow('Invalid challenge')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should return userId if the verifyChallenge is correct', async () => {
|
|
93
|
+
const spyCreateSession = spyOn(Session.prototype, 'create').mockResolvedValue({
|
|
94
|
+
accessToken: 'accessToken',
|
|
95
|
+
refreshToken: 'refreshToken',
|
|
96
|
+
sessionId: 'sessionId',
|
|
97
|
+
csrfToken: 'csrfToken',
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
mockOnVerifyChallenge.mockResolvedValue({ userId: 'userId' } as never)
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
await verifyChallengeResolver(
|
|
104
|
+
undefined,
|
|
105
|
+
{
|
|
106
|
+
input: {
|
|
107
|
+
secondFA: {
|
|
108
|
+
// @ts-expect-error
|
|
109
|
+
fakeOtp: {
|
|
110
|
+
code: '123456',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
context,
|
|
116
|
+
),
|
|
117
|
+
).toEqual({
|
|
118
|
+
accessToken: 'accessToken',
|
|
119
|
+
srp: undefined,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(mockOnVerifyChallenge).toHaveBeenCalledTimes(1)
|
|
123
|
+
expect(mockOnVerifyChallenge).toHaveBeenCalledWith({
|
|
124
|
+
input: { code: '123456' },
|
|
125
|
+
context: expect.any(Object),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(spyCreateSession).toHaveBeenCalledTimes(1)
|
|
129
|
+
expect(spyCreateSession).toHaveBeenCalledWith('userId', context)
|
|
130
|
+
|
|
131
|
+
spyCreateSession.mockRestore()
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { VerifyChallengeInput } from '../../../generated/wabe'
|
|
2
|
+
import type { WabeContext } from '../../server/interface'
|
|
3
|
+
import type { DevWabeTypes } from '../../utils/helper'
|
|
4
|
+
import { Session } from '../Session'
|
|
5
|
+
import type { SecondaryProviderInterface } from '../interface'
|
|
6
|
+
import { getAuthenticationMethod } from '../utils'
|
|
7
|
+
|
|
8
|
+
export const verifyChallengeResolver = async (
|
|
9
|
+
_: any,
|
|
10
|
+
{
|
|
11
|
+
input,
|
|
12
|
+
}: {
|
|
13
|
+
input: VerifyChallengeInput
|
|
14
|
+
},
|
|
15
|
+
context: WabeContext<DevWabeTypes>,
|
|
16
|
+
) => {
|
|
17
|
+
if (!input.secondFA) throw new Error('One factor is required')
|
|
18
|
+
|
|
19
|
+
const listOfFactor = Object.keys(input.secondFA)
|
|
20
|
+
|
|
21
|
+
if (listOfFactor.length > 1) throw new Error('Only one factor is allowed')
|
|
22
|
+
|
|
23
|
+
const { provider, name } = getAuthenticationMethod<any, SecondaryProviderInterface<DevWabeTypes>>(
|
|
24
|
+
listOfFactor,
|
|
25
|
+
context,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const result = await provider.onVerifyChallenge({
|
|
29
|
+
context,
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
input: input.secondFA[name],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!result?.userId) throw new Error('Invalid challenge')
|
|
35
|
+
|
|
36
|
+
const session = new Session()
|
|
37
|
+
|
|
38
|
+
const { accessToken, refreshToken } = await session.create(result.userId, context)
|
|
39
|
+
|
|
40
|
+
if (context.wabe.config.authentication?.session?.cookieSession) {
|
|
41
|
+
const accessTokenExpiresAt = session.getAccessTokenExpireAt(context.wabe.config)
|
|
42
|
+
const refreshTokenExpiresAt = session.getRefreshTokenExpireAt(context.wabe.config)
|
|
43
|
+
|
|
44
|
+
context.response?.setCookie('refreshToken', refreshToken, {
|
|
45
|
+
httpOnly: true,
|
|
46
|
+
path: '/',
|
|
47
|
+
sameSite: 'None',
|
|
48
|
+
secure: true,
|
|
49
|
+
expires: refreshTokenExpiresAt,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
context.response?.setCookie('accessToken', accessToken, {
|
|
53
|
+
httpOnly: true,
|
|
54
|
+
path: '/',
|
|
55
|
+
sameSite: 'None',
|
|
56
|
+
secure: true,
|
|
57
|
+
expires: accessTokenExpiresAt,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { accessToken, srp: result.srp }
|
|
62
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, it, expect } from 'bun:test'
|
|
2
|
+
import type { Wabe } from '../server'
|
|
3
|
+
import type { DevWabeTypes } from '../utils/helper'
|
|
4
|
+
import { initializeRoles } from './roles'
|
|
5
|
+
import { setupTests, closeTests } from '../utils/testHelper'
|
|
6
|
+
|
|
7
|
+
describe('roles', () => {
|
|
8
|
+
let wabe: Wabe<DevWabeTypes>
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
const setup = await setupTests()
|
|
12
|
+
wabe = setup.wabe
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await closeTests(wabe)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should create all roles', async () => {
|
|
20
|
+
await wabe.controllers.database.clearDatabase()
|
|
21
|
+
|
|
22
|
+
await initializeRoles(wabe)
|
|
23
|
+
|
|
24
|
+
const res = await wabe.controllers.database.getObjects({
|
|
25
|
+
className: 'Role',
|
|
26
|
+
context: { isRoot: true, wabe: wabe },
|
|
27
|
+
select: { name: true },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(res.length).toEqual(4)
|
|
31
|
+
expect(res.map((role) => role?.name)).toEqual(['Client', 'Client2', 'Client3', 'Admin'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should not create all roles if there already exist', async () => {
|
|
35
|
+
await wabe.controllers.database.clearDatabase()
|
|
36
|
+
|
|
37
|
+
await initializeRoles(wabe)
|
|
38
|
+
await initializeRoles(wabe)
|
|
39
|
+
|
|
40
|
+
const res = await wabe.controllers.database.getObjects({
|
|
41
|
+
className: 'Role',
|
|
42
|
+
context: { isRoot: true, wabe: wabe },
|
|
43
|
+
select: { name: true },
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
expect(res.length).toEqual(4)
|
|
47
|
+
expect(res.map((role) => role?.name)).toEqual(['Client', 'Client2', 'Client3', 'Admin'])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { notEmpty, type Wabe } from '..'
|
|
2
|
+
import type { DevWabeTypes } from '../utils/helper'
|
|
3
|
+
|
|
4
|
+
export const initializeRoles = async (wabe: Wabe<DevWabeTypes>) => {
|
|
5
|
+
const roles = wabe.config?.authentication?.roles || []
|
|
6
|
+
|
|
7
|
+
if (roles.length === 0) return
|
|
8
|
+
|
|
9
|
+
const res = await wabe.controllers.database.getObjects({
|
|
10
|
+
className: 'Role',
|
|
11
|
+
context: {
|
|
12
|
+
isRoot: true,
|
|
13
|
+
wabe,
|
|
14
|
+
},
|
|
15
|
+
select: { name: true },
|
|
16
|
+
where: {
|
|
17
|
+
name: {
|
|
18
|
+
in: roles,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const alreadyCreatedRoles = res.map((role) => role?.name).filter(notEmpty)
|
|
24
|
+
|
|
25
|
+
const objectsToCreate = roles
|
|
26
|
+
.filter((role) => !alreadyCreatedRoles.includes(role))
|
|
27
|
+
.map((role) => ({ name: role }))
|
|
28
|
+
|
|
29
|
+
if (objectsToCreate.length === 0) return
|
|
30
|
+
|
|
31
|
+
await wabe.controllers.database.createObjects({
|
|
32
|
+
className: 'Role',
|
|
33
|
+
context: {
|
|
34
|
+
isRoot: true,
|
|
35
|
+
wabe,
|
|
36
|
+
},
|
|
37
|
+
data: objectsToCreate,
|
|
38
|
+
select: {},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { getAuthenticationMethod } from './utils'
|
|
3
|
+
|
|
4
|
+
describe('Authentication utils', () => {
|
|
5
|
+
const mockOnSignIn = mock(() => Promise.resolve({}))
|
|
6
|
+
const mockOnSignUp = mock(() => Promise.resolve({}))
|
|
7
|
+
|
|
8
|
+
const mockOnSendChallenge = mock(() => Promise.resolve())
|
|
9
|
+
const mockOnVerifyChallenge = mock(() => Promise.resolve(true))
|
|
10
|
+
|
|
11
|
+
const config = {
|
|
12
|
+
authentication: {
|
|
13
|
+
customAuthenticationMethods: [
|
|
14
|
+
{
|
|
15
|
+
name: 'otp',
|
|
16
|
+
input: {
|
|
17
|
+
code: {
|
|
18
|
+
type: 'String',
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
provider: {
|
|
23
|
+
name: 'otp',
|
|
24
|
+
onSendChallenge: mockOnSendChallenge as any,
|
|
25
|
+
onVerifyChallenge: mockOnVerifyChallenge as any,
|
|
26
|
+
},
|
|
27
|
+
isSecondaryFactor: true,
|
|
28
|
+
dataToStore: {},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'emailPassword',
|
|
32
|
+
input: {
|
|
33
|
+
email: {
|
|
34
|
+
type: 'Email',
|
|
35
|
+
required: true,
|
|
36
|
+
},
|
|
37
|
+
password: {
|
|
38
|
+
type: 'String',
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
provider: {
|
|
43
|
+
name: 'emailPassword',
|
|
44
|
+
onSignUp: mockOnSignIn as any,
|
|
45
|
+
onSignIn: mockOnSignUp as any,
|
|
46
|
+
},
|
|
47
|
+
dataToStore: {},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
} as any
|
|
52
|
+
|
|
53
|
+
it('should throw an error if we provided two authentication methods', () => {
|
|
54
|
+
expect(() =>
|
|
55
|
+
getAuthenticationMethod(['emailPassword', 'otherAuthenticationMethod'], {
|
|
56
|
+
wabe: { config },
|
|
57
|
+
} as any),
|
|
58
|
+
).toThrow('One authentication method is required at the time')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should throw an error if no authentication methods is provided', () => {
|
|
62
|
+
expect(() => getAuthenticationMethod([], { wabe: { config } } as any)).toThrow(
|
|
63
|
+
'One authentication method is required at the time',
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should throw an error if no one authentication method is found', () => {
|
|
68
|
+
expect(() =>
|
|
69
|
+
getAuthenticationMethod(['otherAuthenticationMethod'], {
|
|
70
|
+
wabe: { config },
|
|
71
|
+
} as any),
|
|
72
|
+
).toThrow('No available custom authentication methods found')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should find a secondary factor method', () => {
|
|
76
|
+
expect(getAuthenticationMethod(['otp'], { wabe: { config } } as any)).toEqual({
|
|
77
|
+
name: 'otp',
|
|
78
|
+
input: expect.any(Object),
|
|
79
|
+
provider: expect.any(Object),
|
|
80
|
+
isSecondaryFactor: true,
|
|
81
|
+
dataToStore: {},
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should return the valid authentication method', () => {
|
|
86
|
+
expect(
|
|
87
|
+
getAuthenticationMethod(['emailPassword'], {
|
|
88
|
+
wabe: { config },
|
|
89
|
+
} as any),
|
|
90
|
+
).toEqual({
|
|
91
|
+
name: 'emailPassword',
|
|
92
|
+
input: expect.any(Object),
|
|
93
|
+
provider: expect.any(Object),
|
|
94
|
+
dataToStore: {},
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WabeTypes } from '../server'
|
|
2
|
+
import type { WabeContext } from '../server/interface'
|
|
3
|
+
import type {
|
|
4
|
+
CustomAuthenticationMethods,
|
|
5
|
+
ProviderInterface,
|
|
6
|
+
SecondaryProviderInterface,
|
|
7
|
+
} from './interface'
|
|
8
|
+
|
|
9
|
+
export const getAuthenticationMethod = <
|
|
10
|
+
T extends WabeTypes,
|
|
11
|
+
U = ProviderInterface<T> | SecondaryProviderInterface<T>,
|
|
12
|
+
>(
|
|
13
|
+
listOfMethods: string[],
|
|
14
|
+
context: WabeContext<any>,
|
|
15
|
+
): CustomAuthenticationMethods<T, U> => {
|
|
16
|
+
const customAuthenticationConfig =
|
|
17
|
+
context.wabe.config?.authentication?.customAuthenticationMethods
|
|
18
|
+
|
|
19
|
+
if (!customAuthenticationConfig) throw new Error('No custom authentication methods found')
|
|
20
|
+
|
|
21
|
+
// We remove the secondary factor to only get all authentication methods
|
|
22
|
+
const authenticationMethods = listOfMethods.filter((method) => method !== 'secondaryFactor')
|
|
23
|
+
|
|
24
|
+
// We check if the client don't use multiple authentication methods at the same time
|
|
25
|
+
if (authenticationMethods.length > 1 || authenticationMethods.length === 0)
|
|
26
|
+
throw new Error('One authentication method is required at the time')
|
|
27
|
+
|
|
28
|
+
const authenticationMethod = authenticationMethods[0]
|
|
29
|
+
|
|
30
|
+
// We check if the authentication method is valid
|
|
31
|
+
const validAuthenticationMethod = customAuthenticationConfig.find(
|
|
32
|
+
(method) => method.name.toLowerCase() === authenticationMethod?.toLowerCase(),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (!validAuthenticationMethod)
|
|
36
|
+
throw new Error('No available custom authentication methods found')
|
|
37
|
+
|
|
38
|
+
return validAuthenticationMethod as CustomAuthenticationMethods<T, U>
|
|
39
|
+
}
|