Separate second-factor verification and delivery from auth (#40)
This commit is contained in:
1 parent
12a38cd654
commit
ecb63b61c6
33 files changed
+1772
-123
No files matched your search
@@ -13,6 +13,8 @@ class BackendDTO {
|
||||
'';
|
||||
String get pendingToken =>
|
||||
(data['pendingToken'] as Map<String, dynamic>)['token'] as String;
|
||||
String get challengeId => data['challengeId'] as String? ?? '';
|
||||
String get channel => data['channel'] as String? ?? '';
|
||||
String get target => data['target'] as String;
|
||||
DateTime get expiresAt => DateTime.parse(data['expiresAt'] as String);
|
||||
DateTime get resendAt => DateTime.parse(data['resendAt'] as String);
|
||||
|
||||
@@ -9,15 +9,20 @@ abstract final class BackendMapper {
|
||||
return AuthResult(userId: dto.userId, isSimulated: false);
|
||||
}
|
||||
|
||||
static FactorChallenge challenge(BackendDTO dto, String id) =>
|
||||
FactorChallenge(
|
||||
id: id,
|
||||
purpose: FactorPurpose.login,
|
||||
operation: id,
|
||||
target: dto.target,
|
||||
expiresAt: dto.expiresAt,
|
||||
resendAt: dto.resendAt,
|
||||
);
|
||||
static FactorChallenge challenge(BackendDTO dto, String id) {
|
||||
if (dto.challengeId.isEmpty || dto.channel != 'email') {
|
||||
throw const FactorException(FactorError.unavailable);
|
||||
}
|
||||
return FactorChallenge(
|
||||
id: id,
|
||||
purpose: FactorPurpose.login,
|
||||
operation: id,
|
||||
target: dto.target,
|
||||
expiresAt: dto.expiresAt,
|
||||
resendAt: dto.resendAt,
|
||||
);
|
||||
}
|
||||
|
||||
static AuthSession session(BackendDTO dto) {
|
||||
if (dto.accessToken.isEmpty || dto.userId.isEmpty) {
|
||||
throw const FormatException('Missing session');
|
||||
|
||||
@@ -48,7 +48,7 @@ class BackendAuthServices
|
||||
final String clientId;
|
||||
final http.Client _client;
|
||||
final String _deviceId = _key();
|
||||
final _pending = <String, String>{};
|
||||
final _pending = <String, ({String token, String challengeId})>{};
|
||||
final _grants = <String, AuthSession>{};
|
||||
final _retryKeys = <String, String>{};
|
||||
String? _queuedSignup;
|
||||
@@ -125,7 +125,7 @@ class BackendAuthServices
|
||||
});
|
||||
final id = _key();
|
||||
final challenge = BackendMapper.challenge(dto, id);
|
||||
_pending[id] = dto.pendingToken;
|
||||
_pending[id] = (token: dto.pendingToken, challengeId: dto.challengeId);
|
||||
return AuthResult(userId: '', isSimulated: false, challenge: challenge);
|
||||
}
|
||||
|
||||
@@ -185,11 +185,16 @@ class BackendAuthServices
|
||||
if (pending == null) throw const FactorException(FactorError.expired);
|
||||
final key = _retryKeys.putIfAbsent('login:$challengeId', _key);
|
||||
final dto = await _post('login/resend', {
|
||||
'pendingToken': pending,
|
||||
'pendingToken': pending.token,
|
||||
'challengeId': pending.challengeId,
|
||||
'idempotencyKey': key,
|
||||
}, factor: true);
|
||||
final challenge = BackendMapper.challenge(dto, challengeId);
|
||||
if (dto.challengeId != pending.challengeId) {
|
||||
throw const FactorException(FactorError.unavailable);
|
||||
}
|
||||
_retryKeys.remove('login:$challengeId');
|
||||
return BackendMapper.challenge(dto, challengeId);
|
||||
return challenge;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -197,7 +202,8 @@ class BackendAuthServices
|
||||
final pending = _pending[challengeId];
|
||||
if (pending == null) throw const FactorException(FactorError.expired);
|
||||
final dto = await _post('login/verify', {
|
||||
'pendingToken': pending,
|
||||
'pendingToken': pending.token,
|
||||
'challengeId': pending.challengeId,
|
||||
'code': code,
|
||||
}, factor: true);
|
||||
final session = BackendMapper.session(dto);
|
||||
|
||||
Reference in new issue
Block a user