wabe 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +138 -32
  2. package/bucket/b.txt +1 -0
  3. package/dev/index.ts +215 -0
  4. package/dist/authentication/Session.d.ts +4 -1
  5. package/dist/authentication/interface.d.ts +16 -0
  6. package/dist/email/interface.d.ts +1 -1
  7. package/dist/graphql/resolvers.d.ts +4 -2
  8. package/dist/hooks/index.d.ts +1 -0
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +8713 -8867
  11. package/dist/server/index.d.ts +4 -2
  12. package/dist/utils/crypto.d.ts +7 -0
  13. package/dist/utils/helper.d.ts +4 -1
  14. package/generated/schema.graphql +16 -14
  15. package/generated/wabe.ts +4 -4
  16. package/package.json +15 -15
  17. package/src/authentication/OTP.test.ts +69 -0
  18. package/src/authentication/OTP.ts +66 -0
  19. package/src/authentication/Session.test.ts +665 -0
  20. package/src/authentication/Session.ts +529 -0
  21. package/src/authentication/defaultAuthentication.ts +214 -0
  22. package/src/authentication/index.ts +3 -0
  23. package/src/authentication/interface.ts +157 -0
  24. package/src/authentication/oauth/GitHub.test.ts +105 -0
  25. package/src/authentication/oauth/GitHub.ts +133 -0
  26. package/src/authentication/oauth/Google.test.ts +105 -0
  27. package/src/authentication/oauth/Google.ts +110 -0
  28. package/src/authentication/oauth/Oauth2Client.test.ts +225 -0
  29. package/src/authentication/oauth/Oauth2Client.ts +140 -0
  30. package/src/authentication/oauth/index.ts +2 -0
  31. package/src/authentication/oauth/utils.test.ts +35 -0
  32. package/src/authentication/oauth/utils.ts +28 -0
  33. package/src/authentication/providers/EmailOTP.test.ts +138 -0
  34. package/src/authentication/providers/EmailOTP.ts +93 -0
  35. package/src/authentication/providers/EmailPassword.test.ts +187 -0
  36. package/src/authentication/providers/EmailPassword.ts +130 -0
  37. package/src/authentication/providers/EmailPasswordSRP.test.ts +206 -0
  38. package/src/authentication/providers/EmailPasswordSRP.ts +184 -0
  39. package/src/authentication/providers/GitHub.ts +30 -0
  40. package/src/authentication/providers/Google.ts +30 -0
  41. package/src/authentication/providers/OAuth.test.ts +185 -0
  42. package/src/authentication/providers/OAuth.ts +112 -0
  43. package/src/authentication/providers/PhonePassword.test.ts +187 -0
  44. package/src/authentication/providers/PhonePassword.ts +129 -0
  45. package/src/authentication/providers/QRCodeOTP.test.ts +79 -0
  46. package/src/authentication/providers/QRCodeOTP.ts +65 -0
  47. package/src/authentication/providers/index.ts +6 -0
  48. package/src/authentication/resolvers/refreshResolver.test.ts +37 -0
  49. package/src/authentication/resolvers/refreshResolver.ts +20 -0
  50. package/src/authentication/resolvers/signInWithResolver.inte.test.ts +59 -0
  51. package/src/authentication/resolvers/signInWithResolver.test.ts +307 -0
  52. package/src/authentication/resolvers/signInWithResolver.ts +102 -0
  53. package/src/authentication/resolvers/signOutResolver.test.ts +41 -0
  54. package/src/authentication/resolvers/signOutResolver.ts +22 -0
  55. package/src/authentication/resolvers/signUpWithResolver.test.ts +186 -0
  56. package/src/authentication/resolvers/signUpWithResolver.ts +69 -0
  57. package/src/authentication/resolvers/verifyChallenge.test.ts +136 -0
  58. package/src/authentication/resolvers/verifyChallenge.ts +69 -0
  59. package/src/authentication/roles.test.ts +59 -0
  60. package/src/authentication/roles.ts +40 -0
  61. package/src/authentication/utils.test.ts +99 -0
  62. package/src/authentication/utils.ts +43 -0
  63. package/src/cache/InMemoryCache.test.ts +62 -0
  64. package/src/cache/InMemoryCache.ts +45 -0
  65. package/src/cron/index.test.ts +17 -0
  66. package/src/cron/index.ts +46 -0
  67. package/src/database/DatabaseController.test.ts +625 -0
  68. package/src/database/DatabaseController.ts +983 -0
  69. package/src/database/index.test.ts +1230 -0
  70. package/src/database/index.ts +9 -0
  71. package/src/database/interface.ts +312 -0
  72. package/src/email/DevAdapter.ts +8 -0
  73. package/src/email/EmailController.test.ts +29 -0
  74. package/src/email/EmailController.ts +13 -0
  75. package/src/email/index.ts +2 -0
  76. package/src/email/interface.ts +36 -0
  77. package/src/email/templates/sendOtpCode.ts +120 -0
  78. package/src/file/FileController.ts +28 -0
  79. package/src/file/FileDevAdapter.ts +54 -0
  80. package/src/file/hookDeleteFile.ts +27 -0
  81. package/src/file/hookReadFile.ts +70 -0
  82. package/src/file/hookUploadFile.ts +53 -0
  83. package/src/file/index.test.ts +979 -0
  84. package/src/file/index.ts +2 -0
  85. package/src/file/interface.ts +42 -0
  86. package/src/graphql/GraphQLSchema.test.ts +4399 -0
  87. package/src/graphql/GraphQLSchema.ts +928 -0
  88. package/src/graphql/index.ts +2 -0
  89. package/src/graphql/parseGraphqlSchema.ts +94 -0
  90. package/src/graphql/parser.test.ts +217 -0
  91. package/src/graphql/parser.ts +566 -0
  92. package/src/graphql/pointerAndRelationFunction.ts +200 -0
  93. package/src/graphql/resolvers.ts +467 -0
  94. package/src/graphql/tests/aggregation.test.ts +1123 -0
  95. package/src/graphql/tests/e2e.test.ts +596 -0
  96. package/src/graphql/tests/scalars.test.ts +250 -0
  97. package/src/graphql/types.ts +219 -0
  98. package/src/hooks/HookObject.test.ts +122 -0
  99. package/src/hooks/HookObject.ts +168 -0
  100. package/src/hooks/authentication.ts +76 -0
  101. package/src/hooks/createUser.test.ts +77 -0
  102. package/src/hooks/createUser.ts +10 -0
  103. package/src/hooks/defaultFields.test.ts +187 -0
  104. package/src/hooks/defaultFields.ts +40 -0
  105. package/src/hooks/deleteSession.test.ts +181 -0
  106. package/src/hooks/deleteSession.ts +20 -0
  107. package/src/hooks/hashFieldHook.test.ts +163 -0
  108. package/src/hooks/hashFieldHook.ts +97 -0
  109. package/src/hooks/index.test.ts +207 -0
  110. package/src/hooks/index.ts +430 -0
  111. package/src/hooks/permissions.test.ts +424 -0
  112. package/src/hooks/permissions.ts +113 -0
  113. package/src/hooks/protected.test.ts +551 -0
  114. package/src/hooks/protected.ts +72 -0
  115. package/src/hooks/searchableFields.test.ts +166 -0
  116. package/src/hooks/searchableFields.ts +98 -0
  117. package/src/hooks/session.test.ts +138 -0
  118. package/src/hooks/session.ts +78 -0
  119. package/src/hooks/setEmail.test.ts +216 -0
  120. package/src/hooks/setEmail.ts +35 -0
  121. package/src/hooks/setupAcl.test.ts +589 -0
  122. package/src/hooks/setupAcl.ts +29 -0
  123. package/src/index.ts +9 -0
  124. package/src/schema/Schema.test.ts +484 -0
  125. package/src/schema/Schema.ts +795 -0
  126. package/src/schema/defaultResolvers.ts +94 -0
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/resolvers/meResolver.test.ts +62 -0
  129. package/src/schema/resolvers/meResolver.ts +14 -0
  130. package/src/schema/resolvers/newFile.ts +0 -0
  131. package/src/schema/resolvers/resetPassword.test.ts +345 -0
  132. package/src/schema/resolvers/resetPassword.ts +64 -0
  133. package/src/schema/resolvers/sendEmail.test.ts +118 -0
  134. package/src/schema/resolvers/sendEmail.ts +21 -0
  135. package/src/schema/resolvers/sendOtpCode.test.ts +153 -0
  136. package/src/schema/resolvers/sendOtpCode.ts +52 -0
  137. package/src/security.test.ts +3461 -0
  138. package/src/server/defaultSessionHandler.test.ts +66 -0
  139. package/src/server/defaultSessionHandler.ts +115 -0
  140. package/src/server/generateCodegen.ts +476 -0
  141. package/src/server/index.test.ts +552 -0
  142. package/src/server/index.ts +354 -0
  143. package/src/server/interface.ts +11 -0
  144. package/src/server/routes/authHandler.ts +187 -0
  145. package/src/server/routes/index.ts +40 -0
  146. package/src/utils/crypto.test.ts +41 -0
  147. package/src/utils/crypto.ts +121 -0
  148. package/src/utils/export.ts +13 -0
  149. package/src/utils/helper.ts +195 -0
  150. package/src/utils/index.test.ts +11 -0
  151. package/src/utils/index.ts +201 -0
  152. package/src/utils/preload.ts +8 -0
  153. package/src/utils/testHelper.ts +117 -0
  154. package/tsconfig.json +32 -0
  155. package/bunfig.toml +0 -4
  156. package/dist/ai/index.d.ts +0 -1
  157. package/dist/ai/interface.d.ts +0 -9
  158. /package/dist/server/{defaultHandlers.d.ts → defaultSessionHandler.d.ts} +0 -0
@@ -0,0 +1,214 @@
1
+ import type { WabeTypes } from '..'
2
+ import type {
3
+ CustomAuthenticationMethods,
4
+ ProviderInterface,
5
+ } from './interface'
6
+ import { GitHub, QRCodeOTP } from './providers'
7
+ import { Google } from './providers'
8
+ import { EmailOTP } from './providers/EmailOTP'
9
+ import { EmailPassword } from './providers/EmailPassword'
10
+ import {
11
+ EmailPasswordSRPChallenge,
12
+ EmailPasswordSRP,
13
+ } from './providers/EmailPasswordSRP'
14
+ import { PhonePassword } from './providers/PhonePassword'
15
+
16
+ export const defaultAuthenticationMethods = <
17
+ T extends WabeTypes,
18
+ >(): CustomAuthenticationMethods<T, ProviderInterface<T>>[] => [
19
+ {
20
+ name: 'emailPasswordSRPChallenge',
21
+ input: {
22
+ email: {
23
+ type: 'Email',
24
+ required: true,
25
+ },
26
+ clientPublic: {
27
+ type: 'String',
28
+ required: true,
29
+ },
30
+ clientSessionProof: {
31
+ type: 'String',
32
+ required: true,
33
+ },
34
+ },
35
+ // @ts-expect-error
36
+ provider: new EmailPasswordSRPChallenge(),
37
+ isSecondaryFactor: true,
38
+ },
39
+ {
40
+ name: 'emailPasswordSRP',
41
+ input: {
42
+ email: {
43
+ type: 'Email',
44
+ required: true,
45
+ },
46
+ clientPublic: {
47
+ type: 'String',
48
+ },
49
+ salt: {
50
+ type: 'String',
51
+ },
52
+ verifier: {
53
+ type: 'String',
54
+ },
55
+ },
56
+ dataToStore: {
57
+ email: {
58
+ type: 'Email',
59
+ required: true,
60
+ },
61
+ salt: {
62
+ type: 'String',
63
+ required: true,
64
+ },
65
+ verifier: {
66
+ type: 'String',
67
+ required: true,
68
+ },
69
+ serverSecret: {
70
+ type: 'String',
71
+ },
72
+ },
73
+ // @ts-expect-error
74
+ provider: new EmailPasswordSRP(),
75
+ },
76
+ {
77
+ name: 'emailOTP',
78
+ input: {
79
+ email: {
80
+ type: 'Email',
81
+ required: true,
82
+ },
83
+ otp: {
84
+ type: 'String',
85
+ required: true,
86
+ },
87
+ },
88
+ // @ts-expect-error
89
+ provider: new EmailOTP(),
90
+ isSecondaryFactor: true,
91
+ },
92
+ {
93
+ name: 'qrCodeOTP',
94
+ input: {
95
+ email: {
96
+ type: 'Email',
97
+ required: true,
98
+ },
99
+ otp: {
100
+ type: 'String',
101
+ required: true,
102
+ },
103
+ },
104
+ // @ts-expect-error
105
+ provider: new QRCodeOTP(),
106
+ isSecondaryFactor: true,
107
+ },
108
+ {
109
+ name: 'phonePassword',
110
+ input: {
111
+ phone: {
112
+ type: 'Phone',
113
+ required: true,
114
+ },
115
+ password: {
116
+ type: 'Hash',
117
+ required: true,
118
+ },
119
+ },
120
+ dataToStore: {
121
+ phone: {
122
+ type: 'Phone',
123
+ required: true,
124
+ },
125
+ password: {
126
+ type: 'Hash',
127
+ required: true,
128
+ },
129
+ },
130
+ // @ts-expect-error
131
+ provider: new PhonePassword(),
132
+ },
133
+ {
134
+ name: 'emailPassword',
135
+ input: {
136
+ email: {
137
+ type: 'Email',
138
+ required: true,
139
+ },
140
+ password: {
141
+ type: 'Hash',
142
+ required: true,
143
+ },
144
+ },
145
+ dataToStore: {
146
+ email: {
147
+ type: 'Email',
148
+ required: true,
149
+ },
150
+ password: {
151
+ type: 'Hash',
152
+ required: true,
153
+ },
154
+ },
155
+ // @ts-expect-error
156
+ provider: new EmailPassword(),
157
+ },
158
+ {
159
+ name: 'google',
160
+ input: {
161
+ authorizationCode: {
162
+ type: 'String',
163
+ required: true,
164
+ },
165
+ codeVerifier: {
166
+ type: 'String',
167
+ required: true,
168
+ },
169
+ },
170
+ dataToStore: {
171
+ email: {
172
+ type: 'Email',
173
+ required: true,
174
+ },
175
+ verifiedEmail: {
176
+ type: 'Boolean',
177
+ required: true,
178
+ },
179
+ },
180
+ // There is no signUp method for Google provider
181
+ // @ts-expect-error
182
+ provider: new Google(),
183
+ },
184
+ {
185
+ name: 'github',
186
+ input: {
187
+ authorizationCode: {
188
+ type: 'String',
189
+ required: true,
190
+ },
191
+ codeVerifier: {
192
+ type: 'String',
193
+ required: true,
194
+ },
195
+ },
196
+ dataToStore: {
197
+ email: {
198
+ type: 'Email',
199
+ required: true,
200
+ },
201
+ avatarUrl: {
202
+ type: 'String',
203
+ required: true,
204
+ },
205
+ username: {
206
+ type: 'String',
207
+ required: true,
208
+ },
209
+ },
210
+ // There is no signUp method for Google provider
211
+ // @ts-expect-error
212
+ provider: new GitHub(),
213
+ },
214
+ ]
@@ -0,0 +1,3 @@
1
+ export * from './interface'
2
+ export * from './oauth'
3
+ export * from './OTP'
@@ -0,0 +1,157 @@
1
+ import type { User } from '../../generated/wabe'
2
+ import type { WabeContext } from '../server/interface'
3
+ import type { SchemaFields } from '../schema'
4
+ import type { WabeTypes, WobeCustomContext } from '../server'
5
+ import type { SelectType } from '../database/interface'
6
+
7
+ export enum ProviderEnum {
8
+ google = 'google',
9
+ github = 'github',
10
+ }
11
+
12
+ export interface ProviderConfig {
13
+ clientId: string
14
+ clientSecret: string
15
+ }
16
+
17
+ export type AuthenticationEventsOptions<T extends WabeTypes, K> = {
18
+ context: WabeContext<T>
19
+ input: K
20
+ }
21
+
22
+ export type AuthenticationEventsOptionsWithUserId<
23
+ T extends WabeTypes,
24
+ K,
25
+ > = AuthenticationEventsOptions<T, K> & {
26
+ userId: string
27
+ }
28
+
29
+ export type OnSendChallengeOptions<T extends WabeTypes> = {
30
+ context: WabeContext<T>
31
+ user: T['types']['User']
32
+ }
33
+
34
+ export type OnVerifyChallengeOptions<T extends WabeTypes, K> = {
35
+ context: WabeContext<T>
36
+ input: K
37
+ }
38
+
39
+ export type ProviderInterface<T extends WabeTypes, K = any> = {
40
+ onSignIn: (options: AuthenticationEventsOptions<T, K>) => Promise<{
41
+ user: Partial<User>
42
+ srp?: {
43
+ salt: string
44
+ serverPublic: string
45
+ }
46
+ }>
47
+ onSignUp: (
48
+ options: AuthenticationEventsOptions<T, K>,
49
+ ) => Promise<{ authenticationDataToSave: any }>
50
+ onUpdateAuthenticationData?: (
51
+ options: AuthenticationEventsOptionsWithUserId<T, K>,
52
+ ) => Promise<{ authenticationDataToSave: any }>
53
+ }
54
+
55
+ export type SecondaryProviderInterface<T extends WabeTypes, K = any> = {
56
+ onSendChallenge?: (options: OnSendChallengeOptions<T>) => Promise<void> | void
57
+ onVerifyChallenge: (options: OnVerifyChallengeOptions<T, K>) =>
58
+ | Promise<{
59
+ userId: string
60
+ srp?: { serverSessionProof: string }
61
+ } | null>
62
+ | ({ userId: string; srp?: { serverSessionProof: string } } | null)
63
+ }
64
+
65
+ export type CustomAuthenticationMethods<
66
+ T extends WabeTypes,
67
+ U = ProviderInterface<T> | SecondaryProviderInterface<T>,
68
+ K = SchemaFields<T>,
69
+ W = SchemaFields<T>,
70
+ > = {
71
+ name: string
72
+ input: K
73
+ dataToStore?: W
74
+ provider: U
75
+ isSecondaryFactor?: boolean
76
+ }
77
+
78
+ export type RoleConfig = Array<string>
79
+
80
+ export interface SessionConfig<T extends WabeTypes> {
81
+ /**
82
+ * The time in milliseconds that the access token will expire
83
+ */
84
+ accessTokenExpiresInMs?: number
85
+ /**
86
+ * The time in milliseconds that the refresh token will expire
87
+ */
88
+ refreshTokenExpiresInMs?: number
89
+ /**
90
+ * Set to true to automatically store the session tokens in cookies
91
+ */
92
+ cookieSession?: boolean
93
+ /**
94
+ * The JWT secret used to sign the session tokens
95
+ */
96
+ jwtSecret: string
97
+ /**
98
+ * Optional audience to embed and verify in JWTs
99
+ */
100
+ jwtAudience?: string
101
+ /**
102
+ * Optional issuer to embed and verify in JWTs
103
+ */
104
+ jwtIssuer?: string
105
+ /**
106
+ * Secret dedicated to CSRF token HMAC (defaults to jwtSecret)
107
+ */
108
+ csrfSecret?: string
109
+ /**
110
+ * Secret used to encrypt session tokens at rest (defaults to jwtSecret)
111
+ */
112
+ tokenSecret?: string
113
+ /**
114
+ * A selection of fields to include in the JWT token in the "user" fields
115
+ */
116
+ jwtTokenFields?: SelectType<T, 'User', keyof T['types']['User']>
117
+ }
118
+
119
+ export interface AuthenticationConfig<T extends WabeTypes> {
120
+ session?: SessionConfig<T>
121
+ roles?: RoleConfig
122
+ successRedirectPath?: string
123
+ failureRedirectPath?: string
124
+ frontDomain?: string
125
+ backDomain?: string
126
+ providers?: Partial<Record<ProviderEnum, ProviderConfig>>
127
+ customAuthenticationMethods?: CustomAuthenticationMethods<T>[]
128
+ sessionHandler?: (context: WobeCustomContext<T>) => void | Promise<void>
129
+ disableSignUp?: boolean
130
+ }
131
+
132
+ export interface CreateTokenFromAuthorizationCodeOptions {
133
+ code: string
134
+ }
135
+
136
+ export interface refreshTokenOptions {
137
+ refreshToken: string
138
+ }
139
+
140
+ export interface Provider {
141
+ createTokenFromAuthorizationCode(
142
+ options: CreateTokenFromAuthorizationCodeOptions,
143
+ ): Promise<void>
144
+ refreshToken(options: refreshTokenOptions): Promise<void>
145
+ }
146
+
147
+ export enum AuthenticationProvider {
148
+ GitHub = 'github',
149
+ Google = 'google',
150
+ EmailPassword = 'emailPassword',
151
+ PhonePassword = 'phonePassword',
152
+ }
153
+
154
+ export enum SecondaryFactor {
155
+ EmailOTP = 'emailOTP',
156
+ QRCodeOTP = 'qrcodeOTP',
157
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it, spyOn } from 'bun:test'
2
+ import { GitHub } from './GitHub'
3
+ import { OAuth2Client } from './Oauth2Client'
4
+
5
+ describe('GitHub oauth', () => {
6
+ const config = {
7
+ port: 3001,
8
+ authentication: {
9
+ backDomain: 'api.wabe.dev',
10
+ providers: {
11
+ github: {
12
+ clientId: 'clientId',
13
+ clientSecret: 'clientSecret',
14
+ },
15
+ },
16
+ },
17
+ } as any
18
+
19
+ const githubOauth = new GitHub(config)
20
+
21
+ it('should create authorization url', () => {
22
+ const spyOauth2ClientCreateAuthorizationUrl = spyOn(
23
+ OAuth2Client.prototype,
24
+ 'createAuthorizationURL',
25
+ ).mockReturnValue(new URL('https://url') as never)
26
+
27
+ const authorizationUrl = githubOauth.createAuthorizationURL(
28
+ 'state',
29
+ 'codeVerifier',
30
+ )
31
+
32
+ expect(authorizationUrl.toString()).toBe(
33
+ 'https://url/?access_type=offline&prompt=select_account',
34
+ )
35
+ expect(spyOauth2ClientCreateAuthorizationUrl).toHaveBeenCalledTimes(1)
36
+ expect(spyOauth2ClientCreateAuthorizationUrl).toHaveBeenCalledWith({
37
+ state: 'state',
38
+ codeVerifier: 'codeVerifier',
39
+ scopes: ['read:user', 'user:email'],
40
+ })
41
+
42
+ spyOauth2ClientCreateAuthorizationUrl.mockRestore()
43
+ })
44
+
45
+ it('should validate authorization code', async () => {
46
+ const spyOauth2ClientValidateAuthorizationCode = spyOn(
47
+ OAuth2Client.prototype,
48
+ 'validateAuthorizationCode',
49
+ ).mockResolvedValue({
50
+ access_token: 'access_token',
51
+ refresh_token: 'refresh_token',
52
+ expires_in: 3600,
53
+ })
54
+
55
+ const res = await githubOauth.validateAuthorizationCode(
56
+ 'code',
57
+ 'codeVerifier',
58
+ )
59
+
60
+ expect(spyOauth2ClientValidateAuthorizationCode).toHaveBeenCalledTimes(1)
61
+ expect(spyOauth2ClientValidateAuthorizationCode).toHaveBeenCalledWith(
62
+ 'code',
63
+ {
64
+ authenticateWith: 'request_body',
65
+ credentials: 'clientSecret',
66
+ codeVerifier: 'codeVerifier',
67
+ },
68
+ )
69
+
70
+ // +100 to avoid flaky
71
+ expect(
72
+ (res.accessTokenExpiresAt?.getTime() || 0) + 100,
73
+ ).toBeGreaterThanOrEqual(Date.now() + 3600 * 1000)
74
+
75
+ spyOauth2ClientValidateAuthorizationCode.mockRestore()
76
+ })
77
+
78
+ it('should refresh access token', async () => {
79
+ const spyOauth2ClientRefreshAccessToken = spyOn(
80
+ OAuth2Client.prototype,
81
+ 'refreshAccessToken',
82
+ ).mockResolvedValue({
83
+ access_token: 'access_token',
84
+ expires_in: 3600,
85
+ })
86
+
87
+ const res = await githubOauth.refreshAccessToken('refresh_token')
88
+
89
+ expect(spyOauth2ClientRefreshAccessToken).toHaveBeenCalledTimes(1)
90
+ expect(spyOauth2ClientRefreshAccessToken).toHaveBeenCalledWith(
91
+ 'refresh_token',
92
+ {
93
+ authenticateWith: 'request_body',
94
+ credentials: 'clientSecret',
95
+ },
96
+ )
97
+
98
+ expect(res.accessToken).toBe('access_token')
99
+ expect(res.accessTokenExpiresAt?.getTime()).toBeGreaterThanOrEqual(
100
+ Date.now() + 3600 * 1000,
101
+ )
102
+
103
+ spyOauth2ClientRefreshAccessToken.mockRestore()
104
+ })
105
+ })
@@ -0,0 +1,133 @@
1
+ import { OAuth2Client } from '.'
2
+ import type { WabeConfig } from '../../server'
3
+ import type { OAuth2ProviderWithPKCE, Tokens } from './utils'
4
+
5
+ const authorizeEndpoint = 'https://github.com/login/oauth/authorize'
6
+ const tokenEndpoint = 'https://github.com/login/oauth/access_token'
7
+
8
+ interface AuthorizationCodeResponseBody {
9
+ access_token: string
10
+ refresh_token?: string
11
+ expires_in: number
12
+ id_token: string
13
+ }
14
+
15
+ interface RefreshTokenResponseBody {
16
+ access_token: string
17
+ expires_in: number
18
+ }
19
+
20
+ export class GitHub implements OAuth2ProviderWithPKCE {
21
+ private client: OAuth2Client
22
+ private clientSecret: string
23
+
24
+ constructor(config: WabeConfig<any>) {
25
+ const githubConfig = config.authentication?.providers?.github
26
+
27
+ if (!githubConfig) throw new Error('GitHub config not found')
28
+
29
+ const baseUrl = `http${config.isProduction ? 's' : ''}://${config.authentication?.backDomain || '127.0.0.1:' + config.port || 3001}`
30
+
31
+ const redirectURI = `${baseUrl}/auth/oauth/callback`
32
+
33
+ this.client = new OAuth2Client(
34
+ githubConfig.clientId,
35
+ authorizeEndpoint,
36
+ tokenEndpoint,
37
+ redirectURI,
38
+ )
39
+
40
+ this.clientSecret = githubConfig.clientSecret
41
+ }
42
+
43
+ createAuthorizationURL(
44
+ state: string,
45
+ codeVerifier: string,
46
+ options?: {
47
+ scopes?: string[]
48
+ },
49
+ ): URL {
50
+ const scopes = options?.scopes ?? []
51
+ const url = this.client.createAuthorizationURL({
52
+ state,
53
+ codeVerifier,
54
+ scopes: [...scopes, 'read:user', 'user:email'],
55
+ })
56
+
57
+ url.searchParams.set('access_type', 'offline')
58
+ url.searchParams.set('prompt', 'select_account')
59
+
60
+ return url
61
+ }
62
+
63
+ async validateAuthorizationCode(
64
+ code: string,
65
+ codeVerifier: string,
66
+ ): Promise<Tokens> {
67
+ const { access_token, expires_in, refresh_token, id_token } =
68
+ await this.client.validateAuthorizationCode<AuthorizationCodeResponseBody>(
69
+ code,
70
+ {
71
+ authenticateWith: 'request_body',
72
+ codeVerifier,
73
+ credentials: this.clientSecret,
74
+ },
75
+ )
76
+
77
+ return {
78
+ accessToken: access_token,
79
+ refreshToken: refresh_token,
80
+ accessTokenExpiresAt: new Date(Date.now() + expires_in * 1000),
81
+ idToken: id_token,
82
+ }
83
+ }
84
+
85
+ async refreshAccessToken(refreshToken: string): Promise<Tokens> {
86
+ const { access_token, expires_in } =
87
+ await this.client.refreshAccessToken<RefreshTokenResponseBody>(
88
+ refreshToken,
89
+ {
90
+ authenticateWith: 'request_body',
91
+ credentials: this.clientSecret,
92
+ },
93
+ )
94
+
95
+ return {
96
+ accessToken: access_token,
97
+ accessTokenExpiresAt: new Date(Date.now() + expires_in * 1000),
98
+ }
99
+ }
100
+
101
+ async getUserInfo(accessToken: string) {
102
+ const userInfoResponse = await fetch('https://api.github.com/user', {
103
+ headers: {
104
+ Authorization: `Bearer ${accessToken}`,
105
+ Accept: 'application/vnd.github.v3+json',
106
+ },
107
+ })
108
+
109
+ const userEmailResponse = await fetch(
110
+ 'https://api.github.com/user/emails',
111
+ {
112
+ headers: {
113
+ Authorization: `Bearer ${accessToken}`,
114
+ Accept: 'application/vnd.github.v3+json',
115
+ },
116
+ },
117
+ )
118
+
119
+ if (!userInfoResponse.ok || !userEmailResponse.ok)
120
+ throw new Error('Failed to fetch user information from GitHub')
121
+
122
+ const userInfo = await userInfoResponse.json()
123
+ const userEmails = await userEmailResponse.json()
124
+
125
+ const primaryEmail = userEmails.find((email: any) => email.primary)?.email
126
+
127
+ return {
128
+ email: primaryEmail || null,
129
+ username: userInfo.login,
130
+ avatarUrl: userInfo.avatar_url,
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it, spyOn } from 'bun:test'
2
+ import { Google } from './Google'
3
+ import { OAuth2Client } from './Oauth2Client'
4
+
5
+ describe('Google oauth', () => {
6
+ const config = {
7
+ port: 3001,
8
+ authentication: {
9
+ backDomain: 'api.wabe.com',
10
+ providers: {
11
+ google: {
12
+ clientId: 'clientId',
13
+ clientSecret: 'clientSecret',
14
+ },
15
+ },
16
+ },
17
+ } as any
18
+
19
+ const googleOauth = new Google(config)
20
+
21
+ it('should create authorization url', () => {
22
+ const spyOauth2ClientCreateAuthorizationUrl = spyOn(
23
+ OAuth2Client.prototype,
24
+ 'createAuthorizationURL',
25
+ ).mockReturnValue(new URL('https://url') as never)
26
+
27
+ const authorizationUrl = googleOauth.createAuthorizationURL(
28
+ 'state',
29
+ 'codeVerifier',
30
+ )
31
+
32
+ expect(authorizationUrl.toString()).toBe(
33
+ 'https://url/?access_type=offline&prompt=select_account',
34
+ )
35
+ expect(spyOauth2ClientCreateAuthorizationUrl).toHaveBeenCalledTimes(1)
36
+ expect(spyOauth2ClientCreateAuthorizationUrl).toHaveBeenCalledWith({
37
+ state: 'state',
38
+ codeVerifier: 'codeVerifier',
39
+ scopes: ['openid'],
40
+ })
41
+
42
+ spyOauth2ClientCreateAuthorizationUrl.mockRestore()
43
+ })
44
+
45
+ it('should validate authorization code', async () => {
46
+ const spyOauth2ClientValidateAuthorizationCode = spyOn(
47
+ OAuth2Client.prototype,
48
+ 'validateAuthorizationCode',
49
+ ).mockResolvedValue({
50
+ access_token: 'access_token',
51
+ refresh_token: 'refresh_token',
52
+ expires_in: 3600,
53
+ })
54
+
55
+ const res = await googleOauth.validateAuthorizationCode(
56
+ 'code',
57
+ 'codeVerifier',
58
+ )
59
+
60
+ expect(spyOauth2ClientValidateAuthorizationCode).toHaveBeenCalledTimes(1)
61
+ expect(spyOauth2ClientValidateAuthorizationCode).toHaveBeenCalledWith(
62
+ 'code',
63
+ {
64
+ authenticateWith: 'request_body',
65
+ credentials: 'clientSecret',
66
+ codeVerifier: 'codeVerifier',
67
+ },
68
+ )
69
+
70
+ // +100 to avoid flaky
71
+ expect(
72
+ (res.accessTokenExpiresAt?.getTime() || 0) + 100,
73
+ ).toBeGreaterThanOrEqual(Date.now() + 3600 * 1000)
74
+
75
+ spyOauth2ClientValidateAuthorizationCode.mockRestore()
76
+ })
77
+
78
+ it('should refresh access token', async () => {
79
+ const spyOauth2ClientRefreshAccessToken = spyOn(
80
+ OAuth2Client.prototype,
81
+ 'refreshAccessToken',
82
+ ).mockResolvedValue({
83
+ access_token: 'access_token',
84
+ expires_in: 3600,
85
+ })
86
+
87
+ const res = await googleOauth.refreshAccessToken('refresh_token')
88
+
89
+ expect(spyOauth2ClientRefreshAccessToken).toHaveBeenCalledTimes(1)
90
+ expect(spyOauth2ClientRefreshAccessToken).toHaveBeenCalledWith(
91
+ 'refresh_token',
92
+ {
93
+ authenticateWith: 'request_body',
94
+ credentials: 'clientSecret',
95
+ },
96
+ )
97
+
98
+ expect(res.accessToken).toBe('access_token')
99
+ expect(res.accessTokenExpiresAt?.getTime()).toBeGreaterThanOrEqual(
100
+ Date.now() + 3600 * 1000,
101
+ )
102
+
103
+ spyOauth2ClientRefreshAccessToken.mockRestore()
104
+ })
105
+ })