From 26a1e284b2ee0085fedfddf10a2e1b476386fec3 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Tue, 25 Nov 2025 00:46:11 +0100 Subject: [PATCH] Rewired login confirmation --- .../internal/api/routers/public/login.go | 2 +- .../server/confirmationimp/service.go | 2 +- .../confirmations/login_confirmation.dart | 39 ++++++++++++ .../requests/tokens/session_identifier.dart | 18 ++++++ .../lib/data/mapper/session_identifier.dart | 16 +++++ .../lib/models/confirmation_target.dart | 11 ++++ .../lib/models/session_identifier.dart | 9 --- frontend/pshared/lib/provider/account.dart | 8 ++- frontend/pshared/lib/service/account.dart | 38 ------------ frontend/pshared/lib/service/services.dart | 2 +- .../pshared/lib/service/verification.dart | 60 +++++++++++++++++++ frontend/pweb/lib/providers/two_factor.dart | 8 ++- 12 files changed, 159 insertions(+), 54 deletions(-) create mode 100644 frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart create mode 100644 frontend/pshared/lib/api/requests/tokens/session_identifier.dart create mode 100644 frontend/pshared/lib/data/mapper/session_identifier.dart create mode 100644 frontend/pshared/lib/models/confirmation_target.dart create mode 100644 frontend/pshared/lib/service/verification.dart diff --git a/api/server/internal/api/routers/public/login.go b/api/server/internal/api/routers/public/login.go index c8c90c1..a4eaab6 100644 --- a/api/server/internal/api/routers/public/login.go +++ b/api/server/internal/api/routers/public/login.go @@ -20,7 +20,7 @@ import ( const pendingLoginTTLMinutes = 10 -func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc { +func (pr *PublicRouter) logUserIn(ctx context.Context, _ *http.Request, req *srequest.Login) http.HandlerFunc { // Get the account database entry trimmedLogin := strings.TrimSpace(req.Login) account, err := pr.db.GetByEmail(ctx, strings.ToLower(trimmedLogin)) diff --git a/api/server/internal/server/confirmationimp/service.go b/api/server/internal/server/confirmationimp/service.go index 737e8c8..7f37d99 100644 --- a/api/server/internal/server/confirmationimp/service.go +++ b/api/server/internal/server/confirmationimp/service.go @@ -130,7 +130,7 @@ func (a *ConfirmationAPI) sendCode(account *model.Account, target model.Confirma if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil { a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID)) } - a.logger.Debug("Confirmation code debug dump (do not log in production)", zap.String("code", code)) + a.logger.Debug("Confirmation code debug dump", zap.String("code", code)) } func maskEmail(email string) string { diff --git a/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart b/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart new file mode 100644 index 0000000..df9f2c7 --- /dev/null +++ b/frontend/pshared/lib/api/requests/confirmations/login_confirmation.dart @@ -0,0 +1,39 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/models/confirmation_target.dart'; +import 'package:pshared/api/requests/tokens/session_identifier.dart'; + +part 'login_confirmation.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class LoginConfirmationRequest { + final ConfirmationTarget target; + final String? destination; + + const LoginConfirmationRequest({ + this.target = ConfirmationTarget.login, + this.destination, + }); + + factory LoginConfirmationRequest.fromJson(Map json) => _$LoginConfirmationRequestFromJson(json); + Map toJson() => _$LoginConfirmationRequestToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class LoginConfirmationVerifyRequest { + final ConfirmationTarget target; + final String code; + final String? destination; + final SessionIdentifierDto sessionIdentifier; + + const LoginConfirmationVerifyRequest({ + this.target = ConfirmationTarget.login, + required this.code, + this.destination, + required this.sessionIdentifier, + }); + + factory LoginConfirmationVerifyRequest.fromJson(Map json) => _$LoginConfirmationVerifyRequestFromJson(json); + Map toJson() => _$LoginConfirmationVerifyRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/tokens/session_identifier.dart b/frontend/pshared/lib/api/requests/tokens/session_identifier.dart new file mode 100644 index 0000000..f6d27ae --- /dev/null +++ b/frontend/pshared/lib/api/requests/tokens/session_identifier.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'session_identifier.g.dart'; + + +@JsonSerializable() +class SessionIdentifierDto { + final String clientId; + final String deviceId; + + const SessionIdentifierDto({ + required this.clientId, + required this.deviceId, + }); + + factory SessionIdentifierDto.fromJson(Map json) => _$SessionIdentifierDtoFromJson(json); + Map toJson() => _$SessionIdentifierDtoToJson(this); +} diff --git a/frontend/pshared/lib/data/mapper/session_identifier.dart b/frontend/pshared/lib/data/mapper/session_identifier.dart new file mode 100644 index 0000000..c04cc52 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/session_identifier.dart @@ -0,0 +1,16 @@ +import 'package:pshared/api/requests/tokens/session_identifier.dart'; +import 'package:pshared/models/session_identifier.dart'; + +extension SessionIdentifierMapper on SessionIdentifier { + SessionIdentifierDto toDTO() => SessionIdentifierDto( + clientId: clientId, + deviceId: deviceId, + ); +} + +extension SessionIdentifierDtoMapper on SessionIdentifierDto { + SessionIdentifier toDomain() => SessionIdentifier( + clientId: clientId, + deviceId: deviceId, + ); +} diff --git a/frontend/pshared/lib/models/confirmation_target.dart b/frontend/pshared/lib/models/confirmation_target.dart new file mode 100644 index 0000000..25f196f --- /dev/null +++ b/frontend/pshared/lib/models/confirmation_target.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; + + +/// Targets for confirmation codes. +@JsonEnum(alwaysCreate: true) +enum ConfirmationTarget { + @JsonValue('login') + login, + @JsonValue('payout') + payout, +} diff --git a/frontend/pshared/lib/models/session_identifier.dart b/frontend/pshared/lib/models/session_identifier.dart index 106f8f8..3b330f8 100644 --- a/frontend/pshared/lib/models/session_identifier.dart +++ b/frontend/pshared/lib/models/session_identifier.dart @@ -1,8 +1,3 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'session_identifier.g.dart'; - -@JsonSerializable() class SessionIdentifier { final String clientId; final String deviceId; @@ -11,8 +6,4 @@ class SessionIdentifier { required this.clientId, required this.deviceId, }); - - factory SessionIdentifier.fromJson(Map json) => _$SessionIdentifierFromJson(json); - - Map toJson() => _$SessionIdentifierToJson(this); } diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index fea9b02..9413913 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -15,6 +15,7 @@ import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/account.dart'; import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/verification.dart'; import 'package:pshared/utils/exception.dart'; @@ -77,7 +78,12 @@ class AccountProvider extends ChangeNotifier { _setResource(Resource(data: outcome.account, isLoading: false)); _pickupLocale(outcome.account!.locale); } else { - _pendingLogin = outcome.pending; + final pending = outcome.pending; + if (pending == null) { + throw Exception('Pending login data is missing'); + } + await VerificationService.requestLoginCode(pending); + _pendingLogin = pending; _setResource(_resource.copyWith(isLoading: false)); } return outcome; diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart index facb9a1..69eaee7 100644 --- a/frontend/pshared/lib/service/account.dart +++ b/frontend/pshared/lib/service/account.dart @@ -8,13 +8,10 @@ import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/api/requests/password/change.dart'; import 'package:pshared/api/requests/password/forgot.dart'; import 'package:pshared/api/requests/password/reset.dart'; -import 'package:pshared/api/responses/login.dart'; import 'package:pshared/data/mapper/account/account.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/auth/login_outcome.dart'; -import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/service/authorization/service.dart'; -import 'package:pshared/service/authorization/storage.dart'; import 'package:pshared/service/files.dart'; import 'package:pshared/service/services.dart'; import 'package:pshared/utils/http/requests.dart'; @@ -29,41 +26,6 @@ class AccountService { return AuthorizationService.login(_objectType, login); } - static Future resendLoginCode(PendingLogin pending, {String? destination}) async { - await getPOSTResponse( - _objectType, - 'confirmations/resend', - { - 'target': 'login', - if (destination != null) 'destination': destination, - }, - authToken: pending.pendingToken.token, - ); - } - - static Future confirmLoginCode({ - required PendingLogin pending, - required String code, - String? destination, - }) async { - final response = await getPOSTResponse( - _objectType, - 'confirmations/verify', - { - 'target': 'login', - 'code': code, - if (destination != null) 'destination': destination, - 'sessionIdentifier': pending.session.toJson(), - }, - authToken: pending.pendingToken.token, - ); - - final loginResponse = LoginResponse.fromJson(response); - await AuthorizationStorage.updateToken(loginResponse.accessToken); - await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken); - return loginResponse.account.toDomain(); - } - static Future restore() async { return AuthorizationService.restore(); } diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart index f6c9564..fcf45ee 100644 --- a/frontend/pshared/lib/service/services.dart +++ b/frontend/pshared/lib/service/services.dart @@ -1,7 +1,7 @@ class Services { static const String account = 'accounts'; static const String authorization = 'authorization'; - static const String comments = 'comments'; + static const String confirmations = 'confirmations'; static const String device = 'device'; static const String invitations = 'invitations'; static const String organization = 'organizations'; diff --git a/frontend/pshared/lib/service/verification.dart b/frontend/pshared/lib/service/verification.dart new file mode 100644 index 0000000..082b1d1 --- /dev/null +++ b/frontend/pshared/lib/service/verification.dart @@ -0,0 +1,60 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/api/requests/confirmations/login_confirmation.dart'; +import 'package:pshared/api/responses/login.dart'; +import 'package:pshared/data/mapper/session_identifier.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/data/mapper/account/account.dart'; +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/service/authorization/storage.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/requests.dart'; + + +class VerificationService { + static final _logger = Logger('service.verification'); + static const String _objectType = Services.account; + + static Future requestLoginCode(PendingLogin pending, {String? destination}) async { + _logger.fine('Requesting login confirmation code'); + await getPOSTResponse( + _objectType, + '', + LoginConfirmationRequest(destination: destination).toJson(), + authToken: pending.pendingToken.token, + ); + } + + static Future resendLoginCode(PendingLogin pending, {String? destination}) async { + _logger.fine('Resending login confirmation code'); + await getPOSTResponse( + _objectType, + '/resend', + LoginConfirmationRequest(destination: destination).toJson(), + authToken: pending.pendingToken.token, + ); + } + + static Future confirmLoginCode({ + required PendingLogin pending, + required String code, + String? destination, + }) async { + _logger.fine('Confirming login code'); + final response = await getPOSTResponse( + _objectType, + '/verify', + LoginConfirmationVerifyRequest( + code: code, + destination: destination, + sessionIdentifier: pending.session.toDTO(), + ).toJson(), + authToken: pending.pendingToken.token, + ); + + final loginResponse = LoginResponse.fromJson(response); + await AuthorizationStorage.updateToken(loginResponse.accessToken); + await AuthorizationStorage.updateRefreshToken(loginResponse.refreshToken); + return loginResponse.account.toDomain(); + } +} diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index f23bb52..28327cb 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; + import 'package:logging/logging.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/provider/account.dart'; -import 'package:pshared/service/account.dart'; +import 'package:pshared/service/verification.dart'; + class TwoFactorProvider extends ChangeNotifier { static final _logger = Logger('provider.two_factor'); @@ -35,7 +37,7 @@ class TwoFactorProvider extends ChangeNotifier { if (pending == null) { throw Exception('No pending login available'); } - final account = await AccountService.confirmLoginCode( + final account = await VerificationService.confirmLoginCode( pending: pending, code: code, ); @@ -58,7 +60,7 @@ class TwoFactorProvider extends ChangeNotifier { return; } try { - await AccountService.resendLoginCode(pending); + await VerificationService.resendLoginCode(pending); } catch (e) { _logger.warning('Failed to resend login code', e); _hasError = true;