@massimo.mazzoleni/cognito-max 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2410 -0
- package/dist/chunk-AD7T42HJ.js +3 -0
- package/dist/chunk-AD7T42HJ.js.map +1 -0
- package/dist/chunk-DKPFVGTY.js +683 -0
- package/dist/chunk-DKPFVGTY.js.map +1 -0
- package/dist/chunk-N4OQLBV6.js +135 -0
- package/dist/chunk-N4OQLBV6.js.map +1 -0
- package/dist/client-63FraVdm.d.ts +69 -0
- package/dist/client-BAoL8h4E.d.cts +69 -0
- package/dist/core/index.cjs +696 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/errors-BkUDHleb.d.cts +22 -0
- package/dist/errors-BkUDHleb.d.ts +22 -0
- package/dist/index.cjs +696 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +844 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +104 -0
- package/dist/react/index.d.ts +104 -0
- package/dist/react/index.js +64 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-bxA1vonL.d.cts +113 -0
- package/dist/types-bxA1vonL.d.ts +113 -0
- package/dist/ui/index.cjs +1183 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +241 -0
- package/dist/ui/index.d.ts +241 -0
- package/dist/ui/index.js +1109 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +81 -0
- package/src/core/client.ts +604 -0
- package/src/core/errors.ts +91 -0
- package/src/core/event-bus.ts +41 -0
- package/src/core/index.ts +5 -0
- package/src/core/internal/converters.ts +32 -0
- package/src/core/storage.ts +79 -0
- package/src/core/types.ts +87 -0
- package/src/index.ts +1 -0
- package/src/react/components/ProtectedRoute.tsx +56 -0
- package/src/react/context.tsx +126 -0
- package/src/react/hooks/useAuth.ts +75 -0
- package/src/react/hooks/useMfa.ts +19 -0
- package/src/react/hooks/useSession.ts +16 -0
- package/src/react/hooks/useUser.ts +24 -0
- package/src/react/index.ts +10 -0
- package/src/ui/components/ChangePasswordForm.tsx +105 -0
- package/src/ui/components/ForgotPasswordForm.tsx +159 -0
- package/src/ui/components/MfaSetupWizard.tsx +136 -0
- package/src/ui/components/RegisterForm.tsx +159 -0
- package/src/ui/components/SignInForm.tsx +296 -0
- package/src/ui/hooks/useChangePasswordForm.ts +81 -0
- package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
- package/src/ui/hooks/useMfaSetup.ts +93 -0
- package/src/ui/hooks/useRegisterForm.ts +120 -0
- package/src/ui/hooks/useSignInForm.ts +245 -0
- package/src/ui/index.ts +31 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticationDetails,
|
|
3
|
+
CognitoRefreshToken,
|
|
4
|
+
CognitoUser,
|
|
5
|
+
CognitoUserAttribute,
|
|
6
|
+
CognitoUserPool,
|
|
7
|
+
CognitoUserSession,
|
|
8
|
+
} from 'amazon-cognito-identity-js'
|
|
9
|
+
import {
|
|
10
|
+
CognitoIdentityProviderClient,
|
|
11
|
+
GetUserCommand,
|
|
12
|
+
} from '@aws-sdk/client-cognito-identity-provider'
|
|
13
|
+
|
|
14
|
+
import { TypedEventEmitter } from './event-bus'
|
|
15
|
+
import { mapCognitoError, CognitoAuthError, SessionExpiredError } from './errors'
|
|
16
|
+
import { AutoStorageAdapter } from './storage'
|
|
17
|
+
import { buildAuthSession, buildAuthUser } from './internal/converters'
|
|
18
|
+
import type {
|
|
19
|
+
AuthConfig,
|
|
20
|
+
AuthEvents,
|
|
21
|
+
AuthSession,
|
|
22
|
+
AuthState,
|
|
23
|
+
AuthUser,
|
|
24
|
+
MfaPreference,
|
|
25
|
+
MfaSetupResult,
|
|
26
|
+
MfaType,
|
|
27
|
+
ResolvedAuthConfig,
|
|
28
|
+
SignInResult,
|
|
29
|
+
} from './types'
|
|
30
|
+
|
|
31
|
+
function validateConfig(config: AuthConfig): void {
|
|
32
|
+
const missing = (['userPoolId', 'clientId', 'region'] as const).filter(k => !config[k])
|
|
33
|
+
if (missing.length) {
|
|
34
|
+
throw new CognitoAuthError(
|
|
35
|
+
`AuthConfig incompleto — campi obbligatori mancanti: ${missing.join(', ')}`,
|
|
36
|
+
'INVALID_PARAMETER',
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
if (!/^[a-z]{2}-[a-z]+-\d$/.test(config.region)) {
|
|
40
|
+
throw new CognitoAuthError(
|
|
41
|
+
`AuthConfig.region non valido: "${config.region}" (es. eu-west-1)`,
|
|
42
|
+
'INVALID_PARAMETER',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export class CognitoAuthClient extends TypedEventEmitter<AuthEvents> {
|
|
49
|
+
protected readonly config: ResolvedAuthConfig
|
|
50
|
+
private _state: AuthState = 'idle'
|
|
51
|
+
private readonly _pool: CognitoUserPool
|
|
52
|
+
private _idpClient: CognitoIdentityProviderClient | null = null
|
|
53
|
+
// Mappa challengeSession-id → CognitoUser in-flight (MFA / new-password challenge)
|
|
54
|
+
private readonly _pendingChallenges = new Map<string, CognitoUser>()
|
|
55
|
+
private _refreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
56
|
+
|
|
57
|
+
constructor(config: AuthConfig) {
|
|
58
|
+
super()
|
|
59
|
+
validateConfig(config)
|
|
60
|
+
this.config = {
|
|
61
|
+
autoRefresh: true,
|
|
62
|
+
refreshMarginSeconds: 300,
|
|
63
|
+
totpIssuer: config.clientId,
|
|
64
|
+
storage: new AutoStorageAdapter(),
|
|
65
|
+
...config,
|
|
66
|
+
}
|
|
67
|
+
this._pool = new CognitoUserPool({
|
|
68
|
+
UserPoolId: this.config.userPoolId,
|
|
69
|
+
ClientId: this.config.clientId,
|
|
70
|
+
// amazon-cognito-identity-js ICognitoStorage coincide con il nostro StorageAdapter
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
Storage: this.config.storage as any,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── State ─────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
get state(): AuthState {
|
|
79
|
+
return this._state
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected setState(next: AuthState): void {
|
|
83
|
+
if (this._state === next) return
|
|
84
|
+
this._state = next
|
|
85
|
+
this.emit('stateChanged', next)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Sign-in ───────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
async signIn(email: string, password: string): Promise<SignInResult> {
|
|
91
|
+
this.setState('loading')
|
|
92
|
+
const cognitoUser = this._makeCognitoUser(email)
|
|
93
|
+
const authDetails = new AuthenticationDetails({ Username: email, Password: password })
|
|
94
|
+
|
|
95
|
+
return new Promise<SignInResult>((resolve, reject) => {
|
|
96
|
+
cognitoUser.authenticateUser(authDetails, {
|
|
97
|
+
onSuccess: (session) => {
|
|
98
|
+
resolve(this._onAuthSuccess(cognitoUser, session))
|
|
99
|
+
},
|
|
100
|
+
onFailure: (err) => {
|
|
101
|
+
this.setState('unauthenticated')
|
|
102
|
+
reject(mapCognitoError(err))
|
|
103
|
+
},
|
|
104
|
+
mfaRequired: () => {
|
|
105
|
+
this.setState('mfa_required')
|
|
106
|
+
const challengeSession = this._storeChallengeUser(cognitoUser)
|
|
107
|
+
const result: SignInResult = { status: 'MFA_REQUIRED', mfaType: 'SMS', challengeSession }
|
|
108
|
+
this.emit('mfaRequired', { mfaType: 'SMS', challengeSession })
|
|
109
|
+
resolve(result)
|
|
110
|
+
},
|
|
111
|
+
totpRequired: () => {
|
|
112
|
+
this.setState('mfa_required')
|
|
113
|
+
const challengeSession = this._storeChallengeUser(cognitoUser)
|
|
114
|
+
const result: SignInResult = { status: 'MFA_REQUIRED', mfaType: 'TOTP', challengeSession }
|
|
115
|
+
this.emit('mfaRequired', { mfaType: 'TOTP', challengeSession })
|
|
116
|
+
resolve(result)
|
|
117
|
+
},
|
|
118
|
+
newPasswordRequired: (_userAttributes, requiredAttributes) => {
|
|
119
|
+
this.setState('new_password_required')
|
|
120
|
+
const challengeSession = this._storeChallengeUser(cognitoUser)
|
|
121
|
+
const required: string[] = Array.isArray(requiredAttributes) ? requiredAttributes : []
|
|
122
|
+
this.emit('newPasswordRequired', { requiredAttributes: required, challengeSession })
|
|
123
|
+
resolve({ status: 'NEW_PASSWORD_REQUIRED', requiredAttributes: required, challengeSession })
|
|
124
|
+
},
|
|
125
|
+
// Cognito richiede il setup TOTP prima di completare il login
|
|
126
|
+
mfaSetup: () => {
|
|
127
|
+
this.setState('mfa_required')
|
|
128
|
+
const challengeSession = this._storeChallengeUser(cognitoUser)
|
|
129
|
+
resolve({ status: 'MFA_SETUP_REQUIRED', challengeSession })
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async respondToMfaChallenge(
|
|
136
|
+
challengeSession: string,
|
|
137
|
+
code: string,
|
|
138
|
+
mfaType: MfaType,
|
|
139
|
+
): Promise<SignInResult> {
|
|
140
|
+
const cognitoUser = this._takeChallengeUser(challengeSession)
|
|
141
|
+
const sdkType = mfaType === 'TOTP' ? 'SOFTWARE_TOKEN_MFA' : 'SMS_MFA'
|
|
142
|
+
|
|
143
|
+
return new Promise<SignInResult>((resolve, reject) => {
|
|
144
|
+
cognitoUser.sendMFACode(
|
|
145
|
+
code,
|
|
146
|
+
{
|
|
147
|
+
onSuccess: (session) => resolve(this._onAuthSuccess(cognitoUser, session)),
|
|
148
|
+
onFailure: (err) => {
|
|
149
|
+
this.setState('unauthenticated')
|
|
150
|
+
reject(mapCognitoError(err))
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
sdkType,
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async respondToNewPasswordChallenge(
|
|
159
|
+
challengeSession: string,
|
|
160
|
+
newPassword: string,
|
|
161
|
+
userAttributes?: Record<string, string>,
|
|
162
|
+
): Promise<SignInResult> {
|
|
163
|
+
const cognitoUser = this._takeChallengeUser(challengeSession)
|
|
164
|
+
|
|
165
|
+
return new Promise<SignInResult>((resolve, reject) => {
|
|
166
|
+
cognitoUser.completeNewPasswordChallenge(
|
|
167
|
+
newPassword,
|
|
168
|
+
userAttributes ?? {}, // es. { name, family_name, given_name }
|
|
169
|
+
{
|
|
170
|
+
onSuccess: (session) => resolve(this._onAuthSuccess(cognitoUser, session)),
|
|
171
|
+
onFailure: (err) => {
|
|
172
|
+
this.setState('unauthenticated')
|
|
173
|
+
reject(mapCognitoError(err))
|
|
174
|
+
},
|
|
175
|
+
// Cognito può richiedere MFA anche dopo il cambio password forzato
|
|
176
|
+
mfaRequired: () => {
|
|
177
|
+
this.setState('mfa_required')
|
|
178
|
+
const newSession = this._storeChallengeUser(cognitoUser)
|
|
179
|
+
this.emit('mfaRequired', { mfaType: 'SMS', challengeSession: newSession })
|
|
180
|
+
resolve({ status: 'MFA_REQUIRED', mfaType: 'SMS', challengeSession: newSession })
|
|
181
|
+
},
|
|
182
|
+
totpRequired: () => {
|
|
183
|
+
this.setState('mfa_required')
|
|
184
|
+
const newSession = this._storeChallengeUser(cognitoUser)
|
|
185
|
+
this.emit('mfaRequired', { mfaType: 'TOTP', challengeSession: newSession })
|
|
186
|
+
resolve({ status: 'MFA_REQUIRED', mfaType: 'TOTP', challengeSession: newSession })
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async signOut(global = false): Promise<void> {
|
|
194
|
+
this._clearRefreshTimer()
|
|
195
|
+
this._pendingChallenges.clear()
|
|
196
|
+
|
|
197
|
+
const cognitoUser = this._pool.getCurrentUser()
|
|
198
|
+
if (!cognitoUser) {
|
|
199
|
+
this.setState('unauthenticated')
|
|
200
|
+
this.emit('signedOut')
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (global) {
|
|
205
|
+
await new Promise<void>((resolve, reject) => {
|
|
206
|
+
cognitoUser.globalSignOut({
|
|
207
|
+
onSuccess: () => {
|
|
208
|
+
this.setState('unauthenticated')
|
|
209
|
+
this.emit('signedOut')
|
|
210
|
+
resolve()
|
|
211
|
+
},
|
|
212
|
+
onFailure: (err) => reject(mapCognitoError(err)),
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
cognitoUser.signOut()
|
|
217
|
+
this.setState('unauthenticated')
|
|
218
|
+
this.emit('signedOut')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Registration ──────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
async signUp(
|
|
225
|
+
email: string,
|
|
226
|
+
password: string,
|
|
227
|
+
attributes: Record<string, string> = {},
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const userAttributes = Object.entries({ email, ...attributes }).map(
|
|
230
|
+
([Name, Value]) => new CognitoUserAttribute({ Name, Value }),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
await new Promise<void>((resolve, reject) => {
|
|
234
|
+
this._pool.signUp(email, password, userAttributes, [], (err) => {
|
|
235
|
+
if (err) return reject(mapCognitoError(err))
|
|
236
|
+
resolve()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async confirmSignUp(email: string, code: string): Promise<void> {
|
|
242
|
+
const cognitoUser = this._makeCognitoUser(email)
|
|
243
|
+
await new Promise<void>((resolve, reject) => {
|
|
244
|
+
cognitoUser.confirmRegistration(code, true, (err) => {
|
|
245
|
+
if (err) return reject(mapCognitoError(err))
|
|
246
|
+
resolve()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async resendConfirmationCode(email: string): Promise<void> {
|
|
252
|
+
const cognitoUser = this._makeCognitoUser(email)
|
|
253
|
+
await new Promise<void>((resolve, reject) => {
|
|
254
|
+
cognitoUser.resendConfirmationCode((err) => {
|
|
255
|
+
if (err) return reject(mapCognitoError(err))
|
|
256
|
+
resolve()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Password ──────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
async forgotPassword(email: string): Promise<void> {
|
|
264
|
+
const cognitoUser = this._makeCognitoUser(email)
|
|
265
|
+
await new Promise<void>((resolve, reject) => {
|
|
266
|
+
cognitoUser.forgotPassword({
|
|
267
|
+
// inputVerificationCode → il codice è stato inviato all'email/telefono
|
|
268
|
+
inputVerificationCode: () => resolve(),
|
|
269
|
+
onSuccess: () => resolve(),
|
|
270
|
+
onFailure: (err) => reject(mapCognitoError(err)),
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async confirmForgotPassword(
|
|
276
|
+
email: string,
|
|
277
|
+
code: string,
|
|
278
|
+
newPassword: string,
|
|
279
|
+
): Promise<void> {
|
|
280
|
+
const cognitoUser = this._makeCognitoUser(email)
|
|
281
|
+
await new Promise<void>((resolve, reject) => {
|
|
282
|
+
cognitoUser.confirmPassword(code, newPassword, {
|
|
283
|
+
onSuccess: () => resolve(),
|
|
284
|
+
onFailure: (err) => reject(mapCognitoError(err)),
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
290
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
291
|
+
await new Promise<void>((resolve, reject) => {
|
|
292
|
+
cognitoUser.changePassword(currentPassword, newPassword, (err) => {
|
|
293
|
+
if (err) return reject(mapCognitoError(err))
|
|
294
|
+
resolve()
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Session & User ────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
async getSession(): Promise<AuthSession> {
|
|
302
|
+
const cognitoUser = this._pool.getCurrentUser()
|
|
303
|
+
if (!cognitoUser) throw new SessionExpiredError()
|
|
304
|
+
|
|
305
|
+
const raw = await this._getRawSession(cognitoUser)
|
|
306
|
+
this._scheduleRefresh(cognitoUser, raw)
|
|
307
|
+
return buildAuthSession(raw)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getCurrentUser(): Promise<AuthUser | null> {
|
|
311
|
+
const cognitoUser = this._pool.getCurrentUser()
|
|
312
|
+
if (!cognitoUser) return null
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const session = await this._getRawSession(cognitoUser)
|
|
316
|
+
this.setState('authenticated')
|
|
317
|
+
return buildAuthUser(session, cognitoUser.getUsername())
|
|
318
|
+
} catch {
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async getUserAttributes(): Promise<Record<string, string>> {
|
|
324
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
cognitoUser.getUserAttributes((err, result) => {
|
|
327
|
+
if (err) return reject(mapCognitoError(err))
|
|
328
|
+
resolve(Object.fromEntries((result ?? []).map(a => [a.getName(), a.getValue()])))
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async updateUserAttributes(attributes: Record<string, string>): Promise<void> {
|
|
334
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
335
|
+
const attrs = Object.entries(attributes).map(
|
|
336
|
+
([Name, Value]) => new CognitoUserAttribute({ Name, Value }),
|
|
337
|
+
)
|
|
338
|
+
await new Promise<void>((resolve, reject) => {
|
|
339
|
+
cognitoUser.updateAttributes(attrs, (err) => {
|
|
340
|
+
if (err) return reject(mapCognitoError(err))
|
|
341
|
+
resolve()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async verifyUserAttribute(
|
|
347
|
+
attribute: 'email' | 'phone_number',
|
|
348
|
+
code: string,
|
|
349
|
+
): Promise<void> {
|
|
350
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
351
|
+
await new Promise<void>((resolve, reject) => {
|
|
352
|
+
cognitoUser.verifyAttribute(attribute, code, {
|
|
353
|
+
onSuccess: () => resolve(),
|
|
354
|
+
onFailure: (err) => reject(mapCognitoError(err)),
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async sendAttributeVerificationCode(attribute: 'email' | 'phone_number'): Promise<void> {
|
|
360
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
361
|
+
await new Promise<void>((resolve, reject) => {
|
|
362
|
+
cognitoUser.getAttributeVerificationCode(attribute, {
|
|
363
|
+
onSuccess: () => resolve(),
|
|
364
|
+
onFailure: (err) => reject(mapCognitoError(err)),
|
|
365
|
+
inputVerificationCode: () => resolve(),
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async deleteUser(): Promise<void> {
|
|
371
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
372
|
+
this._clearRefreshTimer()
|
|
373
|
+
await new Promise<void>((resolve, reject) => {
|
|
374
|
+
cognitoUser.deleteUser((err) => {
|
|
375
|
+
if (err) return reject(mapCognitoError(err))
|
|
376
|
+
this.setState('unauthenticated')
|
|
377
|
+
this.emit('signedOut')
|
|
378
|
+
resolve()
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ─── MFA ───────────────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
async setupTotp(): Promise<MfaSetupResult> {
|
|
386
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
387
|
+
return new Promise<MfaSetupResult>((resolve, reject) => {
|
|
388
|
+
cognitoUser.associateSoftwareToken({
|
|
389
|
+
associateSecretCode: (secretCode: string) => {
|
|
390
|
+
const issuer = encodeURIComponent(this.config.totpIssuer)
|
|
391
|
+
const account = encodeURIComponent(cognitoUser.getUsername())
|
|
392
|
+
const qrCodeUri =
|
|
393
|
+
`otpauth://totp/${issuer}:${account}` +
|
|
394
|
+
`?secret=${secretCode}&issuer=${issuer}`
|
|
395
|
+
resolve({ secretCode, qrCodeUri })
|
|
396
|
+
},
|
|
397
|
+
onFailure: (err: Error) => reject(mapCognitoError(err)),
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async verifyTotpSetup(code: string): Promise<void> {
|
|
403
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
404
|
+
return new Promise<void>((resolve, reject) => {
|
|
405
|
+
cognitoUser.verifySoftwareToken(code, this.config.totpIssuer, {
|
|
406
|
+
onSuccess: () => resolve(),
|
|
407
|
+
onFailure: (err: Error) => reject(mapCognitoError(err)),
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async getMfaPreference(): Promise<MfaPreference> {
|
|
413
|
+
const session = await this.getSession()
|
|
414
|
+
try {
|
|
415
|
+
const response = await this._getIdpClient().send(
|
|
416
|
+
new GetUserCommand({ AccessToken: session.accessToken }),
|
|
417
|
+
)
|
|
418
|
+
const enabled: string[] = response.UserMFASettingList ?? []
|
|
419
|
+
const preferred: string | null = response.PreferredMfaSetting ?? null
|
|
420
|
+
const toSdkType = (s: string | null): MfaType | null =>
|
|
421
|
+
s === 'SOFTWARE_TOKEN_MFA' ? 'TOTP' : s === 'SMS_MFA' ? 'SMS' : null
|
|
422
|
+
return {
|
|
423
|
+
enabled: enabled.length > 0,
|
|
424
|
+
preferred: toSdkType(preferred),
|
|
425
|
+
totp: enabled.includes('SOFTWARE_TOKEN_MFA'),
|
|
426
|
+
sms: enabled.includes('SMS_MFA'),
|
|
427
|
+
}
|
|
428
|
+
} catch (err) {
|
|
429
|
+
throw mapCognitoError(err)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async setMfaPreference(type: MfaType): Promise<void> {
|
|
434
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
435
|
+
const smsMfa = { Enabled: type === 'SMS', PreferredMfa: type === 'SMS' }
|
|
436
|
+
const totpMfa = { Enabled: type === 'TOTP', PreferredMfa: type === 'TOTP' }
|
|
437
|
+
return new Promise<void>((resolve, reject) => {
|
|
438
|
+
cognitoUser.setUserMfaPreference(smsMfa, totpMfa, (err: Error | undefined) => {
|
|
439
|
+
if (err) return reject(mapCognitoError(err))
|
|
440
|
+
resolve()
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async disableMfa(): Promise<void> {
|
|
446
|
+
const cognitoUser = await this._getAuthenticatedUser()
|
|
447
|
+
const off = { Enabled: false, PreferredMfa: false }
|
|
448
|
+
return new Promise<void>((resolve, reject) => {
|
|
449
|
+
cognitoUser.setUserMfaPreference(off, off, (err: Error | undefined) => {
|
|
450
|
+
if (err) return reject(mapCognitoError(err))
|
|
451
|
+
resolve()
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ─── TOTP setup durante il challenge login ─────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Ottiene il secretCode/QR URI per il setup TOTP durante il flusso di login
|
|
460
|
+
* (quando signIn restituisce MFA_SETUP_REQUIRED). Non richiede una sessione
|
|
461
|
+
* autenticata: usa il CognitoUser in attesa nel challenge.
|
|
462
|
+
*/
|
|
463
|
+
async setupTotpChallenge(challengeSession: string): Promise<MfaSetupResult> {
|
|
464
|
+
const cognitoUser = this._peekChallengeUser(challengeSession)
|
|
465
|
+
return new Promise<MfaSetupResult>((resolve, reject) => {
|
|
466
|
+
cognitoUser.associateSoftwareToken({
|
|
467
|
+
associateSecretCode: (secretCode: string) => {
|
|
468
|
+
const issuer = encodeURIComponent(this.config.totpIssuer)
|
|
469
|
+
const account = encodeURIComponent(cognitoUser.getUsername())
|
|
470
|
+
const qrCodeUri =
|
|
471
|
+
`otpauth://totp/${issuer}:${account}` +
|
|
472
|
+
`?secret=${secretCode}&issuer=${issuer}`
|
|
473
|
+
resolve({ secretCode, qrCodeUri })
|
|
474
|
+
},
|
|
475
|
+
onFailure: (err: Error) => reject(mapCognitoError(err)),
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Verifica il codice TOTP e completa il login. Dopo questa chiamata la
|
|
482
|
+
* sessione è autenticata e il challenge viene rimosso dalla mappa.
|
|
483
|
+
*/
|
|
484
|
+
async verifyTotpChallenge(challengeSession: string, code: string): Promise<SignInResult> {
|
|
485
|
+
const cognitoUser = this._takeChallengeUser(challengeSession)
|
|
486
|
+
return new Promise<SignInResult>((resolve, reject) => {
|
|
487
|
+
cognitoUser.verifySoftwareToken(code, this.config.totpIssuer, {
|
|
488
|
+
onSuccess: (session: CognitoUserSession) => resolve(this._onAuthSuccess(cognitoUser, session)),
|
|
489
|
+
onFailure: (err: Error) => reject(mapCognitoError(err)),
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Private helpers ───────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
private _getIdpClient(): CognitoIdentityProviderClient {
|
|
497
|
+
if (!this._idpClient) {
|
|
498
|
+
this._idpClient = new CognitoIdentityProviderClient({ region: this.config.region })
|
|
499
|
+
}
|
|
500
|
+
return this._idpClient
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private _makeCognitoUser(username: string): CognitoUser {
|
|
504
|
+
return new CognitoUser({
|
|
505
|
+
Username: username,
|
|
506
|
+
Pool: this._pool,
|
|
507
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
508
|
+
Storage: this.config.storage as any,
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private _storeChallengeUser(cognitoUser: CognitoUser): string {
|
|
513
|
+
const id = crypto.randomUUID()
|
|
514
|
+
this._pendingChallenges.set(id, cognitoUser)
|
|
515
|
+
return id
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private _takeChallengeUser(challengeSession: string): CognitoUser {
|
|
519
|
+
const user = this._pendingChallenges.get(challengeSession)
|
|
520
|
+
if (!user) {
|
|
521
|
+
throw new CognitoAuthError(
|
|
522
|
+
'Challenge session non valida o scaduta',
|
|
523
|
+
'UNKNOWN',
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
this._pendingChallenges.delete(challengeSession)
|
|
527
|
+
return user
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Legge il CognitoUser senza rimuoverlo dalla mappa (per setupTotpChallenge). */
|
|
531
|
+
private _peekChallengeUser(challengeSession: string): CognitoUser {
|
|
532
|
+
const user = this._pendingChallenges.get(challengeSession)
|
|
533
|
+
if (!user) {
|
|
534
|
+
throw new CognitoAuthError(
|
|
535
|
+
'Challenge session non valida o scaduta',
|
|
536
|
+
'UNKNOWN',
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
return user
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private _onAuthSuccess(cognitoUser: CognitoUser, session: CognitoUserSession): SignInResult {
|
|
543
|
+
const authSession = buildAuthSession(session)
|
|
544
|
+
const authUser = buildAuthUser(session, cognitoUser.getUsername())
|
|
545
|
+
this.setState('authenticated')
|
|
546
|
+
this.emit('signedIn', authUser)
|
|
547
|
+
this._scheduleRefresh(cognitoUser, session)
|
|
548
|
+
return { status: 'SUCCESS', user: authUser, session: authSession }
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private _getRawSession(cognitoUser: CognitoUser): Promise<CognitoUserSession> {
|
|
552
|
+
return new Promise((resolve, reject) => {
|
|
553
|
+
cognitoUser.getSession((err: Error | null, session: CognitoUserSession | null) => {
|
|
554
|
+
if (err || !session) {
|
|
555
|
+
return reject(err ? mapCognitoError(err) : new SessionExpiredError())
|
|
556
|
+
}
|
|
557
|
+
if (!session.isValid()) {
|
|
558
|
+
return reject(new SessionExpiredError())
|
|
559
|
+
}
|
|
560
|
+
resolve(session)
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private async _getAuthenticatedUser(): Promise<CognitoUser> {
|
|
566
|
+
const cognitoUser = this._pool.getCurrentUser()
|
|
567
|
+
if (!cognitoUser) throw new SessionExpiredError()
|
|
568
|
+
// getSession gestisce internamente il refresh se il token è scaduto
|
|
569
|
+
await this._getRawSession(cognitoUser)
|
|
570
|
+
return cognitoUser
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private _scheduleRefresh(cognitoUser: CognitoUser, session: CognitoUserSession): void {
|
|
574
|
+
if (!this.config.autoRefresh) return
|
|
575
|
+
this._clearRefreshTimer()
|
|
576
|
+
|
|
577
|
+
const expiresAt = session.getAccessToken().getExpiration() * 1000
|
|
578
|
+
const refreshIn = expiresAt - Date.now() - this.config.refreshMarginSeconds * 1000
|
|
579
|
+
|
|
580
|
+
if (refreshIn <= 0) return
|
|
581
|
+
|
|
582
|
+
this._refreshTimer = setTimeout(() => {
|
|
583
|
+
const refreshToken = new CognitoRefreshToken({
|
|
584
|
+
RefreshToken: session.getRefreshToken().getToken(),
|
|
585
|
+
})
|
|
586
|
+
cognitoUser.refreshSession(refreshToken, (err, newSession: CognitoUserSession) => {
|
|
587
|
+
if (err) {
|
|
588
|
+
this.emit('sessionExpired')
|
|
589
|
+
this.setState('unauthenticated')
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
this.emit('tokenRefreshed', buildAuthSession(newSession))
|
|
593
|
+
this._scheduleRefresh(cognitoUser, newSession)
|
|
594
|
+
})
|
|
595
|
+
}, refreshIn)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private _clearRefreshTimer(): void {
|
|
599
|
+
if (this._refreshTimer !== null) {
|
|
600
|
+
clearTimeout(this._refreshTimer)
|
|
601
|
+
this._refreshTimer = null
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type AuthErrorCode =
|
|
2
|
+
| 'NOT_AUTHORIZED'
|
|
3
|
+
| 'USER_NOT_FOUND'
|
|
4
|
+
| 'USER_NOT_CONFIRMED'
|
|
5
|
+
| 'INVALID_PARAMETER'
|
|
6
|
+
| 'INVALID_PASSWORD'
|
|
7
|
+
| 'CODE_MISMATCH'
|
|
8
|
+
| 'EXPIRED_CODE'
|
|
9
|
+
| 'CODE_DELIVERY_FAILURE'
|
|
10
|
+
| 'LIMIT_EXCEEDED'
|
|
11
|
+
| 'TOO_MANY_REQUESTS'
|
|
12
|
+
| 'TOO_MANY_FAILED_ATTEMPTS'
|
|
13
|
+
| 'PASSWORD_RESET_REQUIRED'
|
|
14
|
+
| 'MFA_METHOD_NOT_FOUND'
|
|
15
|
+
| 'SOFTWARE_TOKEN_MFA_NOT_FOUND'
|
|
16
|
+
| 'SESSION_EXPIRED'
|
|
17
|
+
| 'NETWORK_ERROR'
|
|
18
|
+
| 'UNKNOWN'
|
|
19
|
+
|
|
20
|
+
export class CognitoAuthError extends Error {
|
|
21
|
+
override readonly name = 'CognitoAuthError'
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
message: string,
|
|
25
|
+
public readonly code: AuthErrorCode,
|
|
26
|
+
public readonly originalError?: unknown,
|
|
27
|
+
) {
|
|
28
|
+
super(message)
|
|
29
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SessionExpiredError extends CognitoAuthError {
|
|
34
|
+
constructor() {
|
|
35
|
+
super('La sessione è scaduta, effettua nuovamente il login', 'SESSION_EXPIRED')
|
|
36
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class NotAuthorizedError extends CognitoAuthError {
|
|
41
|
+
constructor(message = 'Credenziali non valide') {
|
|
42
|
+
super(message, 'NOT_AUTHORIZED')
|
|
43
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class UserNotConfirmedError extends CognitoAuthError {
|
|
48
|
+
constructor() {
|
|
49
|
+
super('Account non confermato. Controlla la tua email.', 'USER_NOT_CONFIRMED')
|
|
50
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class InvalidCodeError extends CognitoAuthError {
|
|
55
|
+
constructor(expired = false) {
|
|
56
|
+
super(
|
|
57
|
+
expired ? 'Il codice è scaduto' : 'Codice non valido',
|
|
58
|
+
expired ? 'EXPIRED_CODE' : 'CODE_MISMATCH',
|
|
59
|
+
)
|
|
60
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Maps raw Cognito SDK error names to our typed errors
|
|
65
|
+
export function mapCognitoError(error: unknown): CognitoAuthError {
|
|
66
|
+
if (error instanceof CognitoAuthError) return error
|
|
67
|
+
|
|
68
|
+
const name: string = (error as any)?.name ?? (error as any)?.code ?? ''
|
|
69
|
+
const message: string = (error as any)?.message ?? 'Si è verificato un errore di autenticazione'
|
|
70
|
+
|
|
71
|
+
const codeMap: Record<string, AuthErrorCode> = {
|
|
72
|
+
NotAuthorizedException: 'NOT_AUTHORIZED',
|
|
73
|
+
UserNotFoundException: 'USER_NOT_FOUND',
|
|
74
|
+
UserNotConfirmedException: 'USER_NOT_CONFIRMED',
|
|
75
|
+
InvalidParameterException: 'INVALID_PARAMETER',
|
|
76
|
+
InvalidPasswordException: 'INVALID_PASSWORD',
|
|
77
|
+
CodeMismatchException: 'CODE_MISMATCH',
|
|
78
|
+
ExpiredCodeException: 'EXPIRED_CODE',
|
|
79
|
+
CodeDeliveryFailureException: 'CODE_DELIVERY_FAILURE',
|
|
80
|
+
LimitExceededException: 'LIMIT_EXCEEDED',
|
|
81
|
+
TooManyRequestsException: 'TOO_MANY_REQUESTS',
|
|
82
|
+
TooManyFailedAttemptsException: 'TOO_MANY_FAILED_ATTEMPTS',
|
|
83
|
+
PasswordResetRequiredException: 'PASSWORD_RESET_REQUIRED',
|
|
84
|
+
MFAMethodNotFoundException: 'MFA_METHOD_NOT_FOUND',
|
|
85
|
+
SoftwareTokenMFANotFoundException: 'SOFTWARE_TOKEN_MFA_NOT_FOUND',
|
|
86
|
+
NetworkError: 'NETWORK_ERROR',
|
|
87
|
+
FetchError: 'NETWORK_ERROR',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return new CognitoAuthError(message, codeMap[name] ?? 'UNKNOWN', error)
|
|
91
|
+
}
|