From a7470f27577a3a35d1b12a51c6d967992655f31b Mon Sep 17 00:00:00 2001 From: romanetar Date: Tue, 30 Jan 2024 15:51:48 +0100 Subject: [PATCH 1/5] change OTP input and layout tweaks Signed-off-by: romanetar --- app/libs/Auth/AuthService.php | 12 +- package.json | 1 + resources/js/login/login.js | 223 ++++++++++++++------------- resources/js/login/login.module.scss | 44 +++++- resources/views/auth/login.blade.php | 19 ++- 5 files changed, 175 insertions(+), 124 deletions(-) diff --git a/app/libs/Auth/AuthService.php b/app/libs/Auth/AuthService.php index 21b2653a..0e2ab3f6 100644 --- a/app/libs/Auth/AuthService.php +++ b/app/libs/Auth/AuthService.php @@ -206,7 +206,7 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ ) ); - throw new AuthenticationException("Non existent OTP."); + throw new AuthenticationException("Non existent single-use code."); } $otp->logRedeemAttempt(); @@ -216,23 +216,23 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ return $this->tx_service->transaction(function () use ($otp, $otpClaim, $client, $remember) { if (!$otp->isAlive()) { - throw new AuthenticationException("OTP is expired."); + throw new AuthenticationException("Single-use code is expired."); } if (!$otp->isValid()) { - throw new AuthenticationException("OTP is not valid."); + throw new AuthenticationException("Single-use code is not valid."); } if ($otp->getValue() != $otpClaim->getValue()) { - throw new AuthenticationException("OTP mismatch."); + throw new AuthenticationException("Single-use code mismatch."); } if(!empty($otpClaim->getScope()) && !$otp->allowScope($otpClaim->getScope())) - throw new InvalidOTPException("OTP Requested scopes escalates former scopes."); + throw new InvalidOTPException("Single-use code requested scopes escalates former scopes."); if (($otp->hasClient() && is_null($client)) || ($otp->hasClient() && !is_null($client) && $client->getClientId() != $otp->getClient()->getClientId())) { - throw new AuthenticationException("OTP audience mismatch."); + throw new AuthenticationException("Single-use code audience mismatch."); } $user = $this->getUserByUsername($otp->getUserName()); diff --git a/package.json b/package.json index 5cf25369..49b31238 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "pure": "^2.85.0", "pwstrength-bootstrap": "^3.0.10", "react-google-recaptcha": "^2.1.0", + "react-otp-input": "^3.1.1", "react-password-strength-bar": "^0.4.0", "react-simplemde-editor": "^5.2.0", "simplemde": "^1.11.2", diff --git a/resources/js/login/login.js b/resources/js/login/login.js index 7b0eb15f..11ace49e 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -23,7 +23,8 @@ import { emailValidator } from '../validator'; import Grid from '@material-ui/core/Grid'; import Swal from 'sweetalert2' import Banner from '../components/banner/banner'; -import { handleThirdPartyProvidersVerbiage } from '../utils'; +import OtpInput from 'react-otp-input'; +import {handleThirdPartyProvidersVerbiage} from '../utils'; import styles from './login.module.scss' import "./third_party_identity_providers.scss"; @@ -155,82 +156,76 @@ const PasswordInputForm = ({ } const OTPInputForm = ({ - formAction, - onAuthenticate, - disableInput, - showPassword, - passwordValue, - passwordError, - onUserPasswordChange, - handleClickShowPassword, - handleMouseDownPassword, - userNameValue, - csrfToken, - shouldShowCaptcha, - captchaPublicKey, - onChangeRecaptcha -}) => { + disableInput, + formAction, + onAuthenticate, + otpCode, + otpError, + otpLength, + onCodeChange, + userNameValue, + csrfToken, + shouldShowCaptcha, + captchaPublicKey, + onChangeRecaptcha, + onReset + }) => { return ( -
- - - {showPassword ? : } - - - ) - }} - - /> -

A single-use code was just sent to your email.

- } - label="Remember me" - /> - - - - - {shouldShowCaptcha() && - - } - - + <> +
+
Enter the single-use code sent to your email:
+
+ } + shouldAutoFocus={true} + hasErrored={!otpError} + errorStyle={{border: '1px solid #e5424d'}} + data-testid="otp_code" + /> +
+ {otpError && +

+ } +
+ +
+
+

+ + Sign in using a different e-mail + +

+
+
After you login you will be e-mailed a link to
+
set a password and complete your account.
+
+
+ + + + + + {shouldShowCaptcha() && + + } + + ); } @@ -375,11 +370,14 @@ const ThirdPartyIdentityProviders = ({ thirdPartyProviders, formAction, disableI ); }) } -

If you have a login, you may still choose to use a social login with the same email address to access your account.

+

If you have a login, you may still choose to use a social login with the same email address to + access your account.

); } +const otp_flow = 'otp'; + class LoginPage extends React.Component { constructor(props) { @@ -387,12 +385,14 @@ class LoginPage extends React.Component { this.state = { user_name: props.userName, user_password: '', + otpCode: '', user_pic: props.hasOwnProperty('user_pic') ? props.user_pic : null, user_fullname: props.hasOwnProperty('user_fullname') ? props.user_fullname : null, user_verified: props.hasOwnProperty('user_verified') ? props.user_verified : false, errors: { - email: "", - password: props.authError != "" ? props.authError : "", + email: '', + otp: props.authError != '' ? props.authError : '', + password: props.authError != '' ? props.authError : '', }, captcha_value: '', showPassword: false, @@ -403,6 +403,10 @@ class LoginPage extends React.Component { infoBannerContent: props.infoBannerContent, } + if (props.authError != '' && !this.state.user_fullname) { + this.state.user_fullname = props.userName; + } + if (this.state.errors.password && this.state.errors.password.includes("is not yet verified")) { this.state.errors.password = this.state.errors.password + `Or have another verification email sent to you.`; } @@ -413,6 +417,7 @@ class LoginPage extends React.Component { this.onAuthenticate = this.onAuthenticate.bind(this); this.onChangeRecaptcha = this.onChangeRecaptcha.bind(this); this.onUserPasswordChange = this.onUserPasswordChange.bind(this); + this.onOTPCodeChange = this.onOTPCodeChange.bind(this); this.shouldShowCaptcha = this.shouldShowCaptcha.bind(this); this.handleClickShowPassword = this.handleClickShowPassword.bind(this); this.handleMouseDownPassword = this.handleMouseDownPassword.bind(this); @@ -427,10 +432,11 @@ class LoginPage extends React.Component { let { response } = payload; this.setState({ ...this.state, - authFlow: "otp", + authFlow: otp_flow, errors: { - email: "", - password: "", + email: '', + otp: '', + password: '' }, user_verified: true, user_fullname: user_fullname, @@ -451,17 +457,20 @@ class LoginPage extends React.Component { } onAuthenticate(ev) { - if (this.state.user_password == '') { - let error = 'Password is empty'; - if (this.state.authFlow == 'OTP') { - error = 'Single-use code is empty'; + if (this.state.authFlow === otp_flow) { + if (this.state.otpCode == '') { + this.setState({...this.state, errors: {...this.state.errors, otp: 'Single-use code is empty'}}); + ev.preventDefault(); + return false; } - this.setState({ ...this.state, errors: { ...this.state.errors, password: error } }); + } else if (this.state.user_password == '') { + this.setState({...this.state, errors: {...this.state.errors, password: 'Password is empty'}}); ev.preventDefault(); return false; } + if (this.state.captcha_value == '' && this.shouldShowCaptcha()) { - this.setState({ ...this.state, errors: { ...this.state.errors, password: 'you must check CAPTCHA' } }); + this.setState({...this.state, errors: {...this.state.errors, password: 'you must check CAPTCHA'}}); ev.preventDefault(); return false; } @@ -478,17 +487,21 @@ class LoginPage extends React.Component { } onUserPasswordChange(ev) { - let { errors } = this.state; - let { value, id } = ev.target; + let {errors} = this.state; + let {value, id} = ev.target; if (value == "") // clean error errors[id] = ''; - this.setState({ ...this.state, user_password: value, errors: { ...errors } }); + this.setState({...this.state, user_password: value, errors: {...errors}}); + } + + onOTPCodeChange(value) { + this.setState({...this.state, otpCode: value}); } onValidateEmail(ev) { ev.preventDefault(); - let { user_name } = this.state; + let {user_name} = this.state; user_name = user_name.trim(); if (user_name == '') { @@ -510,6 +523,7 @@ class LoginPage extends React.Component { user_verified: true, errors: { email: '', + otp: '', password: '' }, disableInput: false @@ -542,8 +556,9 @@ class LoginPage extends React.Component { handleDelete() { this.setState({ ...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false, authFlow: "password", errors: { - email: "", - password: "", + email: '', + otp: '', + password: '' }, }); } @@ -564,8 +579,9 @@ class LoginPage extends React.Component {
- {this.props.appName} + {this.props.appName} {this.state.errors.email ? 'Create an account for:' : 'Sign in'} @@ -661,22 +677,21 @@ class LoginPage extends React.Component { // proceed to ask for password ( 2nd step ) <> - + }
diff --git a/resources/js/login/login.module.scss b/resources/js/login/login.module.scss index 48329646..d855e12c 100644 --- a/resources/js/login/login.module.scss +++ b/resources/js/login/login.module.scss @@ -34,11 +34,6 @@ p > a { margin-bottom: 15%; } - .continue_btn { - float: right; - margin-top: 30%; - } - .secondary_btn { min-width: 300px; text-transform: none; @@ -55,8 +50,45 @@ p > a { width: 100%; border: 1px solid; margin-bottom: 5%; - margin-top: 5%; + margin-top: 5%; + } + + .otp_form { + .subtitle { + font-weight: bold; + margin-top: 10px; + } + + .code_input { + margin-top: 20px; + margin-bottom: 20px; + display: flex; + justify-content: center; + + input { + height: 60px; + min-width: 42px; + font-size: 32px; + background-color: #DADADB; + border: 1px solid #AFB0B2; + border-radius: 5px; + padding: 5px; + display: inline-flex; + text-transform: uppercase; + text-align: center; + margin: 0 5px; + } } + + .footer_instructions { + margin-top: 20px; + text-align: center; + + .after_login_instructions { + margin-top: 20px; + } + } + } } .paper_root { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 6599406f..1df5c759 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -38,16 +38,19 @@ flow: 'password', thirdPartyProviders: [ @foreach($supported_providers as $provider => $label) - {label: "{{$label}}", name:"{{$provider}}"}, - @endforeach + { + label: "{{$label}}", name: "{{$provider}}" + }, + @endforeach ], - forgotPasswordAction:'{{ URL::action("Auth\ForgotPasswordController@showLinkRequestForm") }}', - verifyEmailAction:'{{ URL::action("Auth\EmailVerificationController@showVerificationForm") }}', - helpAction:'mailto:{!! Config::get("app.help_email") !!}', - createAccountAction:'{{ URL::action("Auth\RegisterController@showRegistrationForm") }}', - allowNativeAuth: parseInt('{{ Config::get("auth.allows_native_auth", 1) }}') === 1 ? true: false, - showInfoBanner: parseInt('{{ Config::get("app.show_info_banner", 0) }}') === 1 ? true: false, + forgotPasswordAction: '{{ URL::action("Auth\ForgotPasswordController@showLinkRequestForm") }}', + verifyEmailAction: '{{ URL::action("Auth\EmailVerificationController@showVerificationForm") }}', + helpAction: 'mailto:{!! Config::get("app.help_email") !!}', + createAccountAction: '{{ URL::action("Auth\RegisterController@showRegistrationForm") }}', + allowNativeAuth: parseInt('{{ Config::get("auth.allows_native_auth", 1) }}') === 1 ? true : false, + showInfoBanner: parseInt('{{ Config::get("app.show_info_banner", 0) }}') === 1 ? true : false, infoBannerContent: '{!! html_entity_decode(Config::get("app.info_banner_content")) !!}', + otpLength: {{ Config::get("otp.length") }} } @if(Session::has('max_login_attempts_2_show_captcha')) From b975437a804319bb62d71b56890920e6ce7761b5 Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 5 Feb 2024 13:45:56 +0100 Subject: [PATCH 2/5] otp flow change for users without password Signed-off-by: romanetar --- app/Http/Controllers/UserController.php | 3 ++- resources/js/login/login.js | 26 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a334a371..db929f23 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -255,7 +255,8 @@ public function getAccount() return $this->ok( [ 'pic' => $user->getPic(), - 'full_name' => $user->getFullName() + 'full_name' => $user->getFullName(), + 'has_password_set' => $user->hasPasswordSet() ] ); } catch (ValidationException $ex) { diff --git a/resources/js/login/login.js b/resources/js/login/login.js index 11ace49e..7bdd4270 100644 --- a/resources/js/login/login.js +++ b/resources/js/login/login.js @@ -377,6 +377,7 @@ const ThirdPartyIdentityProviders = ({ thirdPartyProviders, formAction, disableI } const otp_flow = 'otp'; +const password_flow = 'password'; class LoginPage extends React.Component { @@ -424,12 +425,11 @@ class LoginPage extends React.Component { this.handleEmitOtpAction = this.handleEmitOtpAction.bind(this); } - handleEmitOtpAction(ev) { - ev.preventDefault(); + emitOtpAction() { let user_fullname = this.state.user_fullname ? this.state.user_fullname : this.state.user_name; emitOTP(this.state.user_name, this.props.token).then((payload) => { - let { response } = payload; + let {response} = payload; this.setState({ ...this.state, authFlow: otp_flow, @@ -442,12 +442,17 @@ class LoginPage extends React.Component { user_fullname: user_fullname, }); }, (error) => { - let { response, status, message } = error; + let {response, status, message} = error; Swal('Oops...', 'Something went wrong!', 'error') }); return false; } + handleEmitOtpAction(ev) { + ev.preventDefault(); + return this.emitOtpAction(); + } + shouldShowCaptcha() { return ( this.props.hasOwnProperty('maxLoginAttempts2ShowCaptcha') && @@ -521,13 +526,20 @@ class LoginPage extends React.Component { user_pic: response.pic, user_fullname: response.full_name, user_verified: true, + authFlow: response.has_password_set ? password_flow : otp_flow, errors: { email: '', otp: '', password: '' }, disableInput: false - }) + }, function () { + //Once the state is updated, it's now possible to trigger emitOtpAction. + //No need to wait for the component to update. + if (!response.has_password_set) { + this.emitOtpAction(); + } + }); }, (error) => { let { response, status, message } = error; @@ -638,7 +650,7 @@ class LoginPage extends React.Component { } } - {this.state.user_verified && this.state.authFlow == 'password' && + {this.state.user_verified && this.state.authFlow == password_flow && // proceed to ask for password ( 2nd step ) <> } - {this.state.user_verified && this.state.authFlow == 'otp' && + {this.state.user_verified && this.state.authFlow == otp_flow && // proceed to ask for password ( 2nd step ) <> Date: Fri, 9 Feb 2024 12:42:00 +0100 Subject: [PATCH 3/5] set password email reminder on otp login Signed-off-by: romanetar --- .env.example | 4 +- app/Mail/OTPRegistrationReminderEmail.php | 44 +++++++++++++++ app/Mail/WelcomeNewUserEmail.php | 2 +- app/libs/Auth/AuthService.php | 14 ++++- config/auth.php | 4 +- ...h2_passwordless_otp_reg_reminder.blade.php | 0 ...passwordless_otp_reg_reminder_fn.blade.php | 56 +++++++++++++++++++ .../welcome_new_user_email_otp_fn.blade.php | 32 +++++++---- 8 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 app/Mail/OTPRegistrationReminderEmail.php create mode 100644 resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php create mode 100644 resources/views/emails/oauth2_passwordless_otp_reg_reminder_fn.blade.php diff --git a/.env.example b/.env.example index 23f6c3cf..d27f9197 100644 --- a/.env.example +++ b/.env.example @@ -107,4 +107,6 @@ AUTH_ALLOWS_NATIVE_AUTH=1 AUTH_ALLOWS_OTP=1 AUTH_ALLOWS_NATIVE_AUTH_CONFIG=1 MAIL_SEND_WELCOME_EMAIL=1 -DEFAULT_PROFILE_IMAGE= \ No newline at end of file +DEFAULT_PROFILE_IMAGE= + +AUTH_PASSWORD_RESET_LIFETIME=1800 \ No newline at end of file diff --git a/app/Mail/OTPRegistrationReminderEmail.php b/app/Mail/OTPRegistrationReminderEmail.php new file mode 100644 index 00000000..240ed8f6 --- /dev/null +++ b/app/Mail/OTPRegistrationReminderEmail.php @@ -0,0 +1,44 @@ +subject = sprintf('[%1$s] Remember to set your password', Config::get('app.app_name')); + $view = 'emails.oauth2_passwordless_otp_reg_reminder'; + + if (Config::get("app.tenant_name") == 'FNTECH') { + $view = 'emails.oauth2_passwordless_otp_reg_reminder_fn'; + } + + Log::debug(sprintf("OTPRegistrationReminderEmail::build to %s", $this->user_email)); + return $this->from(Config::get("mail.from")) + ->to($this->user_email) + ->subject($this->subject) + ->view($view); + } +} diff --git a/app/Mail/WelcomeNewUserEmail.php b/app/Mail/WelcomeNewUserEmail.php index 6b84be6b..8cddcae4 100644 --- a/app/Mail/WelcomeNewUserEmail.php +++ b/app/Mail/WelcomeNewUserEmail.php @@ -24,7 +24,7 @@ * Class WelcomeNewUserEmail * @package App\Mail */ -final class WelcomeNewUserEmail extends Mailable +class WelcomeNewUserEmail extends Mailable { use Queueable, SerializesModels; diff --git a/app/libs/Auth/AuthService.php b/app/libs/Auth/AuthService.php index 0e2ab3f6..6fd221aa 100644 --- a/app/libs/Auth/AuthService.php +++ b/app/libs/Auth/AuthService.php @@ -14,6 +14,8 @@ use App\libs\OAuth2\Exceptions\ReloadSessionException; use App\libs\OAuth2\Repositories\IOAuth2OTPRepository; +use App\Mail\OTPRegistrationReminderEmail; +use App\Mail\WelcomeNewUserEmail; use App\Services\AbstractService; use Auth\Exceptions\AuthenticationException; use Auth\Repositories\IUserRepository; @@ -21,6 +23,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Cookie; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use Models\OAuth2\Client; use Models\OAuth2\OAuth2OTP; @@ -268,16 +271,21 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ foreach ($grants2Revoke as $otp2Revoke){ try { Log::debug(sprintf("AuthService::loginWithOTP revoking otp %s ", $otp2Revoke->getValue())); - if($otp2Revoke->getValue() !== $otpClaim->getValue()) + if ($otp2Revoke->getValue() !== $otpClaim->getValue()) $otp2Revoke->redeem(); - } - catch (Exception $ex){ + } catch (Exception $ex) { Log::warning($ex); } } Auth::login($user, $remember); + if (!$user->hasPasswordSet()) { + $request = $this->auth_user_service->generatePasswordResetRequest($user->getEmail()); + $reset_password_link = $request->getResetLink(); + Mail::queue(new OTPRegistrationReminderEmail($user, $reset_password_link)); + } + return $otp; }); } diff --git a/config/auth.php b/config/auth.php index 632af3eb..61f48a36 100644 --- a/config/auth.php +++ b/config/auth.php @@ -99,10 +99,10 @@ ], // in seconds - 'password_reset_lifetime' => env('AUTH_PASSWORD_RESET_LIFETIME', 600), + 'password_reset_lifetime' => env('AUTH_PASSWORD_RESET_LIFETIME', 1800), 'password_min_length' => env('AUTH_PASSWORD_MIN_LENGTH', 8), 'password_max_length' => env('AUTH_PASSWORD_MAX_LENGTH', 30), - 'verification_email_lifetime' => env ("AUTH_VERIFICATION_EMAIL_LIFETIME", 600), + 'verification_email_lifetime' => env("AUTH_VERIFICATION_EMAIL_LIFETIME", 600), 'allows_native_auth' => env('AUTH_ALLOWS_NATIVE_AUTH', 1), 'allows_native_on_config' => env('AUTH_ALLOWS_NATIVE_AUTH_CONFIG', 1), 'allows_opt_auth' => env('AUTH_ALLOWS_OTP_AUTH', 1), diff --git a/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php b/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php new file mode 100644 index 00000000..e69de29b diff --git a/resources/views/emails/oauth2_passwordless_otp_reg_reminder_fn.blade.php b/resources/views/emails/oauth2_passwordless_otp_reg_reminder_fn.blade.php new file mode 100644 index 00000000..8d57ca14 --- /dev/null +++ b/resources/views/emails/oauth2_passwordless_otp_reg_reminder_fn.blade.php @@ -0,0 +1,56 @@ +@extends('emails.email_layout') + +@section('content') + + + + @if(!empty($site_base_url)) + + @else + + @endif + + + + + + + + +
+
+ Thank you for using a single-use code to verify your email address on + {!! $site_base_url !!}. You now have an {!! Config::get('app.app_name') !!} using the email + address {!!$user_email!!}. +
+
+
+ Thank you for using a single-use code to verify your email address. You now have + an {!! Config::get('app.app_name') !!} using the email address {!!$user_email!!}. +
+
+
+ In order to login more quickly in the future you can set a password (this + link expires in {!! $reset_password_link_lifetime !!} min but you can always use the reset your password option to get a new one). +
+
+
+ An {!! Config::get('app.app_name') !!} is a login you can use to access + all {!! Config::get('app.tenant_name') !!} apps associated with this event and any other event + produced by + {!! Config::get('app.tenant_name') !!}. The apps will ask for your permission to access information + contained in your profile when you login. + Should you have any questions, please email {!! Config::get('app.help_email') !!} + for assistance. +
+
+@stop \ No newline at end of file diff --git a/resources/views/emails/welcome_new_user_email_otp_fn.blade.php b/resources/views/emails/welcome_new_user_email_otp_fn.blade.php index 73d102c2..2172cba5 100644 --- a/resources/views/emails/welcome_new_user_email_otp_fn.blade.php +++ b/resources/views/emails/welcome_new_user_email_otp_fn.blade.php @@ -5,26 +5,38 @@ @if(!empty($site_base_url)) - +
- Thank you for using a one-time-use code to verify your email address on - {!! $site_base_url !!}. You now have an {!! Config::get('app.app_name') !!} using the email address {!!$user_email!!}. + Thank you for using a single-use code to verify your email address on + {!! $site_base_url !!}. You now have an {!! Config::get('app.app_name') !!} using the email + address {!!$user_email!!}.
@else - +
- Thank you for using a one-time-use code to verify your email address. You now have an {!! Config::get('app.app_name') !!} using the email address {!!$user_email!!}. + Thank you for using a single-use code to verify your email address. You now have + an {!! Config::get('app.app_name') !!} using the email address {!!$user_email!!}.
@endif @if(!empty($reset_password_link)) - - -
In order to login more quickly in the future you can set a password (this link expires in {!! $reset_password_link_lifetime !!} min but you can always use the reset your password option to get a new one).
- - + + +
+ In order to login more quickly in the future you can set a password (this + link expires in {!! $reset_password_link_lifetime !!} min but you can always use the reset your password option to get a new one). +
+ + @endif From b650532d28c01e118ab85a13fc4fa66d933bfaf5 Mon Sep 17 00:00:00 2001 From: "smarcet@gmail.com" Date: Fri, 16 Feb 2024 16:35:35 -0300 Subject: [PATCH 4/5] fix: refactored code Change-Id: Ieedb7e7be88d84030c1995e6e482ea355f9e6066 --- app/Jobs/GenerateOTPRegistrationReminder.php | 55 +++++++++++++++++++ app/Mail/WelcomeNewUserEmail.php | 7 ++- app/Services/Auth/IUserService.php | 7 +++ app/Services/Auth/UserService.php | 21 +++++++ app/libs/Auth/AuthService.php | 6 +- ...h2_passwordless_otp_reg_reminder.blade.php | 50 +++++++++++++++++ 6 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/GenerateOTPRegistrationReminder.php diff --git a/app/Jobs/GenerateOTPRegistrationReminder.php b/app/Jobs/GenerateOTPRegistrationReminder.php new file mode 100644 index 00000000..64886f65 --- /dev/null +++ b/app/Jobs/GenerateOTPRegistrationReminder.php @@ -0,0 +1,55 @@ +user_id = $user->getId(); + Log::debug(sprintf("GenerateOTPRegistrationReminder::GenerateOTPRegistrationReminder user %s", $user->getEmail())); + } + + /** + * @param IAuthUserService $service + * @return void + * @throws \Exception + */ + public function handle(IAuthUserService $service) + { + Log::debug(sprintf("GenerateOTPRegistrationReminder::handle user %s", $this->user_id)); + $service->sendOTPRegistrationReminder($this->user_id); + } + + public function failed(\Throwable $exception) + { + Log::error($exception); + } +} \ No newline at end of file diff --git a/app/Mail/WelcomeNewUserEmail.php b/app/Mail/WelcomeNewUserEmail.php index 8cddcae4..7d255b04 100644 --- a/app/Mail/WelcomeNewUserEmail.php +++ b/app/Mail/WelcomeNewUserEmail.php @@ -15,7 +15,6 @@ use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\URL; @@ -101,8 +100,10 @@ public function __construct if($user->createdByOTP()){ $this->user_created_by_otp = true; $otp = $user->getCreatedByOtp(); - $otp_redirect_url = $otp->getRedirectUrl(); - $this->site_base_url = !empty($otp_redirect_url) ? parse_url($otp_redirect_url)['host'] : null; + if(!is_null($otp)) { + $otp_redirect_url = $otp->getRedirectUrl(); + $this->site_base_url = !empty($otp_redirect_url) ? parse_url($otp_redirect_url)['host'] : null; + } } $this->reset_password_link = $reset_password_link; diff --git a/app/Services/Auth/IUserService.php b/app/Services/Auth/IUserService.php index 7a9a0d85..81b8d209 100644 --- a/app/Services/Auth/IUserService.php +++ b/app/Services/Auth/IUserService.php @@ -129,4 +129,11 @@ public function initializeUser(int $user_id):?User; * @throws \Exception */ public function updateRegistrationRequest(int $id, array $payload):UserRegistrationRequest; + + /** + * @param int $user_id + * @return void + * @throws \Exception + */ + public function sendOTPRegistrationReminder(int $user_id); } \ No newline at end of file diff --git a/app/Services/Auth/UserService.php b/app/Services/Auth/UserService.php index 7490cc10..c708248f 100644 --- a/app/Services/Auth/UserService.php +++ b/app/Services/Auth/UserService.php @@ -23,6 +23,7 @@ use App\libs\Auth\Repositories\ISpamEstimatorFeedRepository; use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository; use App\libs\Auth\Repositories\IUserRegistrationRequestRepository; +use App\Mail\OTPRegistrationReminderEmail; use App\Mail\UserEmailVerificationRequest; use App\Mail\UserEmailVerificationSuccess; use App\Mail\UserPasswordResetRequestMail; @@ -560,4 +561,24 @@ public function initializeUser(int $user_id): ?User return $user; }); } + + /** + * @param int $user_id + * @return void + * @throws \Exception + */ + public function sendOTPRegistrationReminder(int $user_id){ + $this->tx_service->transaction(function() use($user_id) { + Log::debug(sprintf("UserService::sendOTPRegistrationReminder %s", $user_id)); + $user = $this->user_repository->getById($user_id); + if( !$user instanceof User) + throw new EntityNotFoundException(sprintf("User %s not found.", $user_id)); + + if ($user->hasPasswordSet()) + throw new ValidationException(sprintf("User %s already has password set.", $user->getId())); + + $request = $this->generatePasswordResetRequest($user->getEmail()); + Mail::queue(new OTPRegistrationReminderEmail($user, $request->getResetLink())); + }); + } } \ No newline at end of file diff --git a/app/libs/Auth/AuthService.php b/app/libs/Auth/AuthService.php index 6fd221aa..23d910f0 100644 --- a/app/libs/Auth/AuthService.php +++ b/app/libs/Auth/AuthService.php @@ -12,6 +12,7 @@ * limitations under the License. **/ +use App\Jobs\GenerateOTPRegistrationReminder; use App\libs\OAuth2\Exceptions\ReloadSessionException; use App\libs\OAuth2\Repositories\IOAuth2OTPRepository; use App\Mail\OTPRegistrationReminderEmail; @@ -281,9 +282,8 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ Auth::login($user, $remember); if (!$user->hasPasswordSet()) { - $request = $this->auth_user_service->generatePasswordResetRequest($user->getEmail()); - $reset_password_link = $request->getResetLink(); - Mail::queue(new OTPRegistrationReminderEmail($user, $reset_password_link)); + // trigger background job + GenerateOTPRegistrationReminder::dispatch($user); } return $otp; diff --git a/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php b/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php index e69de29b..b0e8df06 100644 --- a/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php +++ b/resources/views/emails/oauth2_passwordless_otp_reg_reminder.blade.php @@ -0,0 +1,50 @@ +@extends('emails.email_layout') + +@section('content') + + + + @if(!empty($site_base_url)) + + @else + + @endif + + + + + + + + +
+
+ Thank you for using a single-use code to verify your email address on + {!! $site_base_url !!}. You now have an {!! Config::get('app.app_name') !!} using the email + address {!!$user_email!!}. +
+
+
+ Thank you for using a single-use code to verify your email address. You now have + an {!! Config::get('app.app_name') !!} using the email address {!!$user_email!!}. +
+
+
+ In order to login more quickly in the future you can set a password (this + link expires in {!! $reset_password_link_lifetime !!} min but you can always use the reset your password option to get a new one). +
+
+
+ You will use this account to access all {!! Config::get('app.tenant_name') !!} community apps and websites that require an {!! Config::get('app.app_name') !!}, + including the virtual Open Infrastructure Summit. Your user details are associated with your {!! Config::get('app.app_name') !!} and you + are able to grant access to that information to each app at your discretion. +
+
+@stop \ No newline at end of file From 7876d92fc965c67031c22aa2749425d16dd8f174 Mon Sep 17 00:00:00 2001 From: "smarcet@gmail.com" Date: Fri, 23 Feb 2024 13:10:53 -0300 Subject: [PATCH 5/5] fix: check new user condition to send OTP reminder Change-Id: I547db8f9b308776e201aee1c9907c7ad21ae7c11 --- app/Http/Controllers/Auth/ResetPasswordController.php | 3 --- app/Providers/EventServiceProvider.php | 4 ++-- app/Services/Auth/UserService.php | 4 ++-- app/libs/Auth/AuthService.php | 9 +++------ 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index e9e754d6..b4ba998a 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -15,10 +15,7 @@ use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository; use App\Services\Auth\IUserService; use Auth\Exceptions\UserPasswordResetRequestVoidException; -use Auth\Repositories\IUserRepository; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Redirect; -use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\Validator; use Illuminate\Http\Request as LaravelRequest; use models\exceptions\EntityNotFoundException; diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 95d1438b..be0eea2d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -75,7 +75,7 @@ public function boot() Event::listen(UserEmailVerified::class, function($event) { $service = App::make(IUserService::class); - if(is_null($service) || !$service instanceof IUserService) return; + if(!$service instanceof IUserService) return; $service->sendSuccessfulVerificationEmail($event->getUserId()); }); @@ -83,7 +83,7 @@ public function boot() { // new user created $service = App::make(IUserService::class); - if(is_null($service) || !$service instanceof IUserService) return; + if(!$service instanceof IUserService) return; $service->initializeUser($event->getUserId()); }); diff --git a/app/Services/Auth/UserService.php b/app/Services/Auth/UserService.php index c708248f..cb78e80e 100644 --- a/app/Services/Auth/UserService.php +++ b/app/Services/Auth/UserService.php @@ -500,7 +500,7 @@ public function sendSuccessfulVerificationEmail(int $user_id): ?User return $this->tx_service->transaction(function() use($user_id){ $user = $this->user_repository->getById($user_id); - if(is_null($user) || !$user instanceof User) return null; + if(!$user instanceof User) return null; $reset_password_link = null; @@ -528,7 +528,7 @@ public function initializeUser(int $user_id): ?User return $this->tx_service->transaction(function() use($user_id) { Log::debug(sprintf("UserService::initializeUser %s", $user_id)); $user = $this->user_repository->getById($user_id); - if(is_null($user) || !$user instanceof User) return null; + if(!$user instanceof User) return null; if(!$user->isEmailVerified()) { Log::debug(sprintf("UserService::initializeUser %s email not verified", $user_id)); diff --git a/app/libs/Auth/AuthService.php b/app/libs/Auth/AuthService.php index 23d910f0..de7d1198 100644 --- a/app/libs/Auth/AuthService.php +++ b/app/libs/Auth/AuthService.php @@ -15,8 +15,6 @@ use App\Jobs\GenerateOTPRegistrationReminder; use App\libs\OAuth2\Exceptions\ReloadSessionException; use App\libs\OAuth2\Repositories\IOAuth2OTPRepository; -use App\Mail\OTPRegistrationReminderEmail; -use App\Mail\WelcomeNewUserEmail; use App\Services\AbstractService; use Auth\Exceptions\AuthenticationException; use Auth\Repositories\IUserRepository; @@ -24,7 +22,6 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Cookie; -use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use Models\OAuth2\Client; use Models\OAuth2\OAuth2OTP; @@ -240,8 +237,8 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ } $user = $this->getUserByUsername($otp->getUserName()); - - if (is_null($user)) { + $new_user = is_null($user); + if ($new_user) { // we need to create a new one ( auto register) Log::debug(sprintf("AuthService::loginWithOTP user %s does not exists ...", $otp->getUserName())); @@ -281,7 +278,7 @@ public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null, bool $ Auth::login($user, $remember); - if (!$user->hasPasswordSet()) { + if (!$user->hasPasswordSet() && !$new_user) { // trigger background job GenerateOTPRegistrationReminder::dispatch($user); }