Skip to content

Instantly share code, notes, and snippets.

@erdum
Created March 12, 2024 04:45
Show Gist options
  • Save erdum/7d050a60066ad285afc3ac47e81a0f16 to your computer and use it in GitHub Desktop.
Save erdum/7d050a60066ad285afc3ac47e81a0f16 to your computer and use it in GitHub Desktop.
Authentication Service Class for Laravel REST API
<?php
namespace App\Services;
use Kreait\Firebase\Factory;
use App\Jobs\ProcessEmail;
use App\Exceptions\InvalidIdTokenException;
use App\Exceptions\UserAlreadyRegisteredException;
use App\Exceptions\UserBlockedException;
use App\Exceptions\AccessForbiddenException;
use App\Exceptions\InvalidCredentialsException;
use App\Exceptions\PasswordNotSetException;
use App\Exceptions\UnverifiedEmailException;
use App\Exceptions\IncorrectOtpException;
use App\Exceptions\OtpExpiredException;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
class FirebaseAuthService
{
protected $auth;
public function __construct()
{
$firebase = (new Factory)->withServiceAccount(
base_path()
. DIRECTORY_SEPARATOR
. config('firebase.projects.app.credentials')
);
$this->auth = $firebase->createAuth();
}
public function social_register($id_token, $is_business)
{
try {
$verified_id_token = $this->auth->verifyIdToken($id_token);
} catch (\Kreait\Firebase\Exception\Auth\FailedToVerifyToken $error) {
throw new InvalidIdTokenException();
}
$trying_uid = $verified_id_token->claims()->get('sub');
$trying_user = $this->auth->getUser($trying_uid);
if (is_user_already_registered($trying_user->email)) {
throw new UserAlreadyRegisteredException();
}
$user = User::updateOrCreate(
['email' => $trying_user->email],
[
'name' => $trying_user->displayName,
'uid' => $trying_user->uid,
'email_verified_at' => now(),
'balance' => 0
]
);
if ($is_business) {
$user->type = 'store_owner';
$user->save();
}
return ['message' => 'User has been successfully registered'];
}
public function social_login($id_token)
{
try {
$verified_id_token = $this->auth->verifyIdToken($id_token);
} catch (\Kreait\Firebase\Exception\Auth\FailedToVerifyToken $error) {
throw new InvalidIdTokenException();
}
$trying_uid = $verified_id_token->claims()->get('sub');
$trying_user = $this->auth->getUser($trying_uid);
// if ($trying_user->password != null) {
// throw new \Exception(
// 'User has created the account with email & password',
// 400
// );
// }
if (empty(User::where('email', $trying_user->email)->first())) {
throw new UserNotFoundException();
}
$user = User::where('email', $trying_user->email)
->where('uid', $trying_user->uid)->first();
if (empty($user)) {
throw new UserNotFoundException();
}
if ($user?->is_blocked) {
throw new UserBlockedException();
}
if ($user?->type == 'admin') {
throw new AccessForbiddenException();
}
$user->uid = $trying_uid;
$user->save();
// $user->tokens()->delete();
return ['token' => $this->generate_token($user)];
}
public function email_register($full_name, $email, $password, $is_business)
{
if ($this->is_user_already_registered($email)) {
throw new UserAlreadyRegisteredException();
}
$uid = null;
try {
$created_user = $this->auth->createUser([
'displayName' => $full_name,
'email' => $email,
'password' => $password,
'emailVerified' => false
]);
$uid = $created_user->uid;
} catch (\Kreait\Firebase\Exception\Auth\EmailExists $error) {
$trying_user = $this->auth->getUserByEmail($email);
$updated_user = $this->auth->updateUser($trying_user->uid, [
'displayName' => $full_name,
'password' => $password
]);
$uid = $updated_user->uid;
}
$user = User::updateOrCreate(
['email' => $email],
[
'name' => $full_name,
'password' => hash('sha256', $password),
'uid' => $uid,
'email_verified_at' => null,
'balance' => 0
]
);
if ($is_business) {
$user->type = 'store_owner';
$user->save();
}
$this->send_otp($email, true);
return [
'message' => 'OTP has been successfully sent to the user email'
];
}
public function email_login($email, $password)
{
$user = User::where('email', $email)->first();
if ($user?->is_blocked) {
throw new UserBlockedException();
}
if ($user?->type == 'admin') {
throw new AccessForbiddenException();
}
if (empty($user)) {
throw new InvalidCredentialsException();
}
if ($user->email_verified_at == null) {
throw new UnverifiedEmailException();
}
if ($user->password == null) {
throw new PasswordNotSetException();
}
if (!hash_equals(hash('sha256', $password), $user->password)) {
throw new InvalidCredentialsException();
}
try {
$trying_user = $this->auth->getUserByEmail($email);
$user->uid = $trying_user->uid;
} catch (\Kreait\Firebase\Exception\Auth\UserNotFound $error) {
$created_user = $this->auth->createUser([
'displayName' => $user->name,
'email' => $email,
'password' => $password,
'emailVerified' => true
]);
$user->uid = $created_user->uid;
}
$user->save();
// $user->tokens()->delete();
return ['token' => $this->generate_token($user)];
}
public function admin_login($email, $password)
{
$user = User::where('email', $email)->first();
if ($user?->type != 'admin') {
throw new AccessForbiddenException();
}
if (empty($user)) {
throw new InvalidCredentialsException();
}
if (!hash_equals(hash('sha256', $password), $user->password)) {
throw new InvalidCredentialsException();
}
try {
$trying_user = $this->auth->getUserByEmail($email);
$user->uid = $trying_user->uid;
} catch (\Kreait\Firebase\Exception\Auth\UserNotFound $error) {
$created_user = $this->auth->createUser([
'displayName' => $user->name,
'email' => $email,
'password' => $password,
'emailVerified' => true
]);
$user->uid = $created_user->uid;
}
$user->save();
// $user->tokens()->delete();
return ['token' => $this->generate_token($user)];
}
public function forget_password($email)
{
$user = User::where('email', $email)->first();
if (empty($user)) {
throw new UserNotFoundException();
}
if ($user->password == null) {
throw new PasswordNotSetException();
}
$this->send_otp($email);
return [
'message' => 'User email has been successfully verified and OTP has been sent.'
];
}
public function verify_otp($otp)
{
$user = User::where('otp', $otp)->first();
if (empty($user)) {
throw new IncorrectOtpException();
}
if (
$user->otp_expiry != null &&
$user->otp_expiry->diffInSeconds(now(), false) >= 0
) {
throw new OtpExpiredException();
}
if ($user->email_verified_at == null) {
$user->otp = null;
$user->otp_expiry = null;
$user->otp_retries = 0;
}
$this->auth->updateUser($user->uid, [
'emailVerified' => true
]);
$user->tokens()->delete();
$user->email_verified_at = now();
$user->save();
return [
'message' => 'OTP has been successfully verified, call login api to get token'
];
}
public function resend_otp($email)
{
$this->send_otp($email);
return ['message' => 'OTP has been successfully sent'];
}
public function update_forgotten_passowrd($otp, $new_password)
{
$user = User::where('otp', $otp)->first();
if (empty($user)) {
throw new IncorrectOtpException();
}
if (
$user->otp_expiry != null &&
$user->otp_expiry->diffInSeconds(now(), false) >= 0
) {
throw new OtpExpiredException();
}
if ($user->password == null) {
throw new PasswordNotSetException();
}
try {
$trying_user = $this->auth->getUserByEmail($user->email);
$this->auth->updateUser($trying_user->uid, [
'password' => $new_password
]);
} catch (\Exception $error) {
throw new \Exception(
'Error while updating user password in Firebase',
500
);
}
$user->password = hash('sha256', $new_password);
$user->save();
ProcessEmail::dispatch(
'Password Reset | ' . config('app.name'),
"Hi, welcome user!\nYour password has been updated",
$user->email
);
return ['message' => 'User password has been successfully updated'];
}
public function update_user_password($password, $new_password)
{
// if ($user->password == null) {
// throw new PasswordNotSetException();
// }
if (!hash_equals(hash('sha256', $password), $user->password)) {
throw new \Exception('Incorrect password', 401);
}
try {
$this->auth->updateUser($user->uid, [
'password' => $new_password
]);
} catch (\Exception $error) {
throw new \Exception(
'Error while updating user password in Firebase',
500
);
}
$user->password = hash('sha256', $new_password);
$user->save();
ProcessEmail::dispatch(
'Password Reset | ' . config('app.name'),
"Hi, welcome user!\nYour password has been updated",
$user->email
);
return ['message' => 'User password has been successfully updated'];
}
public function logout($user)
{
$user->tokens()->delete();
$user->otp = null;
$user->save();
return ['message' => 'User successfully logged out'];
}
public function validate_token()
{
return ['is_token_validate' => (bool) auth('sanctum')->check()];
}
protected function is_user_already_registered($email)
{
return User::where('email', $email)->whereNotNull('email_verified_at')
->count() > 0;
}
protected function generate_token($user)
{
$token = $user->createToken($user->email)->plainTextToken;
$user->save();
return $token;
}
protected function send_otp($email, $skip_expiration = false)
{
$user = User::where('email', $email)->first();
$otp_expiration_diff = $user->otp_expiry?->diffInSeconds(now(), false);
if ($user->otp_expiry != null && $otp_expiration_diff <= 0) {
throw new \Exception(
"You have exceeded the OTP resend limit, please try again after {$user->otp_expiry?->diff(now())->format("%I:%S")}",
429
);
}
if ($user->otp_retries >= config('app.otp_max_retries')) {
$user->otp_retries = 0;
$user->save();
}
$otp = rand(100000, 999999);
ProcessEmail::dispatch(
'OTP | ' . config('app.name'),
"Hello {$user->name},\n\nHere is your One-Time Password (OTP) for authentication:\n\n{$otp}.\n\nPlease use this code to complete your login or transaction.\n\nThank you,\n" . config('app.name'),
$email
);
$user->otp = $otp;
$user->otp_retries++;
if ($skip_expiration) {
$user->otp_expiry = null;
} else {
$user->otp_expiry = now()->addMinutes(config('app.otp_expiration'));
}
if ($user->otp_retries >= config('app.otp_max_retries')) {
$user->otp_expiry = now()->addMinutes(config(
'app.otp_expiration_after_max_tries'
));
}
$user->save();
return $otp;
}
protected function send_email($subject, $content, $email)
{
Mail::raw($content, function ($message) use ($email, $subject) {
$message->to($email)
->subject($subject);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment