@@ -131,63 +131,24 @@ public function isUserLogged()
131131 }
132132
133133 /**
134- * @param OAuth2OTP $otpClaim
135- * @param Client|null $client
136- * @param bool $remember
137- * @return OAuth2OTP|null
138- * @throws Exception
139- */
140- public function loginWithOTP (OAuth2OTP $ otpClaim , ?Client $ client = null , bool $ remember = false ): ?OAuth2OTP
141- {
142-
143- Log::debug (sprintf ("AuthService::loginWithOTP otp %s user %s " , $ otpClaim ->getValue (), $ otpClaim ->getUserName ()));
144-
145- $ otp = $ this ->verifyOTPChallenge (
146- $ otpClaim ->getValue (),
147- $ otpClaim ->getUserName (),
148- $ otpClaim ->getConnection (),
149- true ,
150- $ otpClaim ->getScope (),
151- $ client
152- );
153-
154- $ user = $ otp ->getUser ();
155-
156- if (is_null ($ user )){
157- throw new AuthenticationException ("Non existent user. " );
158- }
159-
160- Auth::login ($ user , $ remember );
161-
162- Log::debug (sprintf ("AuthService::verifyOTPChallenge user %s logged in. " , $ user ->getId ()));
163-
164- return $ otp ;
165- }
166-
167- /**
168- * @param string $otp_value
169- * @param string $user_name
170- * @param string $otp_conn
171- * @param bool $should_create_user
172- * @param string|null $otp_required_scopes
173- * @param Client|null $client
174- * @return OAuth2OTP|null
175- * @throws Exception
134+ * Finds the OTP by value/connection/username, logs the redeem attempt (TX-A),
135+ * then validates lifecycle / value / scope / audience (TX-B).
136+ * TX-A is committed independently so the brute-force counter increments even when TX-B throws.
137+ *
138+ * @throws AuthenticationException
139+ * @throws InvalidOTPException
176140 */
177- public function verifyOTPChallenge
178- (
141+ private function findAndValidateOTP (
179142 string $ otp_value ,
180143 string $ user_name ,
181144 string $ otp_conn ,
182- bool $ should_create_user = true ,
183- ?string $ otp_required_scopes = null ,
184- ?Client $ client = null
185- ): ?OAuth2OTP
145+ ?string $ otp_required_scopes ,
146+ ?Client $ client
147+ ): OAuth2OTP
186148 {
187- Log:: debug ( sprintf ( " AuthService::verifyOTPChallenge otp_value %s user %s " , $ otp_value , $ user_name ));
149+ // TX-A: find + log attempt (committed before any validation can throw)
188150 $ otp = $ this ->tx_service ->transaction (function () use ($ otp_value , $ otp_conn , $ user_name , $ client ) {
189151
190- // find by value, connection and user name
191152 $ otp = $ this ->otp_repository ->getByValueConnectionAndUserName (
192153 $ otp_value ,
193154 $ otp_conn ,
@@ -196,25 +157,20 @@ public function verifyOTPChallenge
196157 );
197158
198159 if (is_null ($ otp )) {
199- // otp no emitted
200- Log::warning
201- (
202- sprintf
203- (
204- "AuthService::verifyOTPChallenge otp %s user %s grant not found " ,
205- $ otp_value ,
206- $ user_name
207- )
208- );
209-
160+ Log::warning (sprintf (
161+ "AuthService::findAndValidateOTP otp %s user %s grant not found " ,
162+ $ otp_value ,
163+ $ user_name
164+ ));
210165 throw new AuthenticationException ("Non existent single-use code. " );
211166 }
212167
213168 $ otp ->logRedeemAttempt ();
214169 return $ otp ;
215170 });
216171
217- return $ this ->tx_service ->transaction (function () use ($ otp , $ otp_value , $ otp_required_scopes , $ user_name , $ should_create_user , $ client ) {
172+ // TX-B: lifecycle / value / scope / audience checks
173+ return $ this ->tx_service ->transaction (function () use ($ otp , $ otp_value , $ otp_required_scopes , $ client ) {
218174
219175 if (!$ otp ->isAlive ()) {
220176 throw new AuthenticationException ("Single-use code is expired. " );
@@ -228,77 +184,135 @@ public function verifyOTPChallenge
228184 throw new AuthenticationException ("Single-use code mismatch. " );
229185 }
230186
231- if (!empty ($ otp_required_scopes ) && !$ otp ->allowScope ($ otp_required_scopes ))
187+ if (!empty ($ otp_required_scopes ) && !$ otp ->allowScope ($ otp_required_scopes )) {
232188 throw new InvalidOTPException ("Single-use code requested scopes escalates former scopes. " );
189+ }
233190
234191 if (($ otp ->hasClient () && is_null ($ client )) ||
235192 ($ otp ->hasClient () && !is_null ($ client ) && $ client ->getClientId () != $ otp ->getClient ()->getClientId ())) {
236193 throw new AuthenticationException ("Single-use code audience mismatch. " );
237194 }
238195
196+ return $ otp ;
197+ });
198+ }
199+
200+ /**
201+ * Marks the OTP redeemed, attaches the user (transient), revokes sibling pending OTPs.
202+ * Entity methods short-circuit for inline OTPs — no special-casing needed here.
203+ */
204+ private function finalizeRedemption (OAuth2OTP $ otp , User $ user , ?Client $ client ): void
205+ {
206+ $ otp ->setAuthTime (time ());
207+ $ otp ->setUserId ($ user ->getId ());
208+ $ otp ->setUser ($ user );
209+ $ otp ->redeem ();
210+
211+ $ grants2Revoke = $ this ->otp_repository ->getByUserNameNotRedeemed ($ otp ->getUserName (), $ client );
212+ foreach ($ grants2Revoke as $ otp2Revoke ) {
213+ try {
214+ Log::debug (sprintf ("AuthService::finalizeRedemption revoking otp %s " , $ otp2Revoke ->getValue ()));
215+ if ($ otp2Revoke ->getValue () !== $ otp ->getValue ())
216+ $ otp2Revoke ->redeem ();
217+ } catch (Exception $ ex ) {
218+ Log::warning ($ ex );
219+ }
220+ }
221+ }
222+
223+ /**
224+ * @param OAuth2OTP $otpClaim
225+ * @param Client|null $client
226+ * @param bool $remember
227+ * @return OAuth2OTP|null
228+ * @throws Exception
229+ */
230+ public function loginWithOTP (OAuth2OTP $ otpClaim , ?Client $ client = null , bool $ remember = false ): ?OAuth2OTP
231+ {
232+ Log::debug (sprintf ("AuthService::loginWithOTP otp %s user %s " , $ otpClaim ->getValue (), $ otpClaim ->getUserName ()));
233+
234+ $ otp = $ this ->findAndValidateOTP (
235+ $ otpClaim ->getValue (),
236+ $ otpClaim ->getUserName (),
237+ $ otpClaim ->getConnection (),
238+ $ otpClaim ->getScope (),
239+ $ client
240+ );
241+
242+ // TX-C: resolve or create user, finalize, login
243+ return $ this ->tx_service ->transaction (function () use ($ otp , $ otpClaim , $ client , $ remember ) {
244+
239245 $ user = $ this ->getUserByUsername ($ otp ->getUserName ());
240- $ new_user = is_null ($ user );
241- if ($ new_user ) {
242-
243- if (!$ should_create_user ) {
244- Log::warning (sprintf (
245- "AuthService::verifyOTPChallenge user %s not found and auto-create disabled. " ,
246- $ otp ->getUserName ()
247- ));
248- throw new AuthenticationException ("We are sorry, your username or password does not match an existing record. " );
249- }
250- // we need to create a new one ( auto register)
251-
252- Log::debug (sprintf ("AuthService::verifyOTPChallenge user %s does not exists ... " , $ otp ->getUserName ()));
253-
254- $ user = $ this ->auth_user_service ->registerUser
255- (
246+
247+ if (is_null ($ user )) {
248+ Log::debug (sprintf ("AuthService::loginWithOTP user %s does not exist; auto-registering. " , $ otp ->getUserName ()));
249+ $ user = $ this ->auth_user_service ->registerUser (
256250 [
257251 'email ' => $ otp ->getUserName (),
258252 'email_verified ' => true ,
259- // dont send email
260253 'send_email_verified_notice ' => false ,
261254 'active ' => true ,
262255 ],
263256 $ otp
264257 );
265- } else {
266- if ($ user ->isActive ()) {
267- // verify email
268- $ user ->verifyEmail (false );
269- }
258+ } else if ($ user ->isActive ()) {
259+ $ user ->verifyEmail (false );
270260 }
271261
272262 if (!$ user ->canLogin ()) {
273- Log::warning (sprintf ("AuthService::verifyOTPChallenge user %s cannot login ( is not active ). " , $ user ->getId ()));
263+ Log::warning (sprintf ("AuthService::loginWithOTP user %s cannot login (not active). " , $ user ->getId ()));
274264 throw new AuthenticationException ("We are sorry, your username or password does not match an existing record. " );
275265 }
276266
277- $ otp ->setAuthTime (time ());
278- $ otp ->setUserId ($ user ->getId ());
279- $ otp ->setUser ($ user );
280- $ otp ->redeem ();
267+ $ this ->finalizeRedemption ($ otp , $ user , $ client );
281268
282- // revoke former ones
283- $ grants2Revoke = $ this ->otp_repository ->getByUserNameNotRedeemed
284- (
285- $ user_name ,
286- $ client
287- );
269+ Auth::login ($ user , $ remember );
270+ Log::debug (sprintf ("AuthService::loginWithOTP user %s logged in. " , $ user ->getId ()));
271+ return $ otp ;
272+ });
273+ }
274+
275+ /**
276+ * Verifies an OTP against an EXISTING user. Marks the OTP redeemed.
277+ * Does NOT auto-register users and does NOT call Auth::login.
278+ * Intended as the strict primitive for MFA challenge verification.
279+ *
280+ * @param OAuth2OTP $otpClaim
281+ * @param Client|null $client
282+ * @return OAuth2OTP
283+ * @throws AuthenticationException when the OTP is invalid or the user does not exist / cannot login.
284+ * @throws InvalidOTPException when the OTP requested scopes escalate.
285+ */
286+ public function verifyOTPChallenge (OAuth2OTP $ otpClaim , ?Client $ client = null ): OAuth2OTP
287+ {
288+ Log::debug (sprintf ("AuthService::verifyOTPChallenge otp %s user %s " , $ otpClaim ->getValue (), $ otpClaim ->getUserName ()));
289+
290+ $ otp = $ this ->findAndValidateOTP (
291+ $ otpClaim ->getValue (),
292+ $ otpClaim ->getUserName (),
293+ $ otpClaim ->getConnection (),
294+ $ otpClaim ->getScope (),
295+ $ client
296+ );
297+
298+ // TX-C: resolve existing user, finalize
299+ return $ this ->tx_service ->transaction (function () use ($ otp , $ client ) {
288300
289- foreach ( $ grants2Revoke as $ otp2Revoke ) {
290- try {
291- Log:: debug ( sprintf ( " AuthService::verifyOTPChallenge revoking otp %s " , $ otp2Revoke -> getValue ()));
292- if ( $ otp2Revoke -> getValue () !== $ otp_value )
293- $ otp2Revoke -> redeem ();
294- } catch ( Exception $ ex ) {
295- Log:: warning ( $ ex );
296- }
301+ $ user = $ this -> getUserByUsername ( $ otp -> getUserName ());
302+
303+ if ( is_null ( $ user ) || ! $ user -> canLogin ()) {
304+ Log:: warning ( sprintf (
305+ " AuthService::verifyOTPChallenge user %s not found or cannot login. " ,
306+ $ otp -> getUserName ()
307+ ) );
308+ throw new AuthenticationException ( " We are sorry, your username or password does not match an existing record. " );
297309 }
298310
311+ $ this ->finalizeRedemption ($ otp , $ user , $ client );
312+
313+ Log::debug (sprintf ("AuthService::verifyOTPChallenge user %s verified. " , $ user ->getId ()));
299314 return $ otp ;
300315 });
301-
302316 }
303317
304318 /**
0 commit comments