486 lines
14 KiB
PHP
486 lines
14 KiB
PHP
<?php namespace Auth;
|
|
/**
|
|
* Copyright 2016 OpenStack Foundation
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
use App\libs\OAuth2\Exceptions\ReloadSessionException;
|
|
use App\libs\OAuth2\Repositories\IOAuth2OTPRepository;
|
|
use App\Services\AbstractService;
|
|
use Auth\Exceptions\AuthenticationException;
|
|
use Auth\Repositories\IUserRepository;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\Cookie;
|
|
use Illuminate\Support\Facades\Session;
|
|
use Models\OAuth2\Client;
|
|
use Models\OAuth2\OAuth2OTP;
|
|
use OAuth2\Exceptions\InvalidOTPException;
|
|
use OAuth2\Models\IClient;
|
|
use OAuth2\Services\IPrincipalService;
|
|
use OpenId\Services\IUserService;
|
|
use App\Services\Auth\IUserService as IAuthUserService;
|
|
use utils\Base64UrlRepresentation;
|
|
use Utils\Db\ITransactionService;
|
|
use Utils\Services\IAuthService;
|
|
use Utils\Services\ICacheService;
|
|
use jwe\compression_algorithms\CompressionAlgorithms_Registry;
|
|
use jwe\compression_algorithms\CompressionAlgorithmsNames;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Class AuthService
|
|
* @package Auth
|
|
*/
|
|
final class AuthService extends AbstractService implements IAuthService
|
|
{
|
|
/**
|
|
* @var IPrincipalService
|
|
*/
|
|
private $principal_service;
|
|
/**
|
|
* @var IUserService
|
|
*/
|
|
private $user_service;
|
|
/**
|
|
* @var ICacheService
|
|
*/
|
|
private $cache_service;
|
|
|
|
/**
|
|
* @var IUserRepository
|
|
*/
|
|
private $user_repository;
|
|
|
|
/**
|
|
* @var IAuthUserService
|
|
*/
|
|
private $auth_user_service;
|
|
|
|
/**
|
|
* @var IOAuth2OTPRepository
|
|
*/
|
|
private $otp_repository;
|
|
|
|
/**
|
|
* AuthService constructor.
|
|
* @param IUserRepository $user_repository
|
|
* @param IOAuth2OTPRepository $otp_repository
|
|
* @param IPrincipalService $principal_service
|
|
* @param IUserService $user_service
|
|
* @param ICacheService $cache_service
|
|
* @param IAuthUserService $auth_user_service
|
|
* @param ITransactionService $tx_service
|
|
*/
|
|
public function __construct
|
|
(
|
|
IUserRepository $user_repository,
|
|
IOAuth2OTPRepository $otp_repository,
|
|
IPrincipalService $principal_service,
|
|
IUserService $user_service,
|
|
ICacheService $cache_service,
|
|
IAuthUserService $auth_user_service,
|
|
ITransactionService $tx_service
|
|
)
|
|
{
|
|
parent::__construct($tx_service);
|
|
$this->user_repository = $user_repository;
|
|
$this->principal_service = $principal_service;
|
|
$this->user_service = $user_service;
|
|
$this->cache_service = $cache_service;
|
|
$this->auth_user_service = $auth_user_service;
|
|
$this->otp_repository = $otp_repository;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isUserLogged()
|
|
{
|
|
return Auth::check();
|
|
}
|
|
|
|
/**
|
|
* @return User|null
|
|
*/
|
|
public function getCurrentUser(): ?User
|
|
{
|
|
return Auth::user();
|
|
}
|
|
|
|
/**
|
|
* @param string $username
|
|
* @param string $password
|
|
* @param bool $remember_me
|
|
* @return bool
|
|
* @throws AuthenticationException
|
|
*/
|
|
public function login(string $username, string $password, bool $remember_me): bool
|
|
{
|
|
Log::debug("AuthService::login");
|
|
|
|
$this->last_login_error = "";
|
|
if (!Auth::attempt(['username' => $username, 'password' => $password], $remember_me)) {
|
|
throw new AuthenticationException("We are sorry, your username or password does not match an existing record.");
|
|
}
|
|
|
|
Log::debug("AuthService::login: clearing principal");
|
|
$this->principal_service->clear();
|
|
$this->principal_service->register
|
|
(
|
|
$this->getCurrentUser()->getId(),
|
|
time()
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param OAuth2OTP $otpClaim
|
|
* @param Client|null $client
|
|
* @return OAuth2OTP|null
|
|
* @throws AuthenticationException
|
|
*/
|
|
public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null): ?OAuth2OTP{
|
|
|
|
$otp = $this->tx_service->transaction(function() use($otpClaim, $client){
|
|
// find first db OTP by connection , by username (email/phone) number and client not redeemed
|
|
$otp = $this->otp_repository->getByConnectionAndUserNameNotRedeemed
|
|
(
|
|
$otpClaim->getConnection(),
|
|
$otpClaim->getUserName(),
|
|
$client
|
|
);
|
|
if(is_null($otp)){
|
|
// otp no emitted
|
|
throw new AuthenticationException("Non existent OTP.");
|
|
}
|
|
$otp->logRedeemAttempt();
|
|
return $otp;
|
|
});
|
|
|
|
return $this->tx_service->transaction(function() use($otp, $otpClaim, $client){
|
|
|
|
if (!$otp->isAlive()) {
|
|
throw new AuthenticationException("OTP is expired.");
|
|
}
|
|
|
|
if (!$otp->isValid()) {
|
|
throw new AuthenticationException( "OTP is not valid.");
|
|
}
|
|
|
|
if ($otp->getValue() != $otpClaim->getValue()) {
|
|
throw new AuthenticationException("OTP mismatch.");
|
|
}
|
|
|
|
if(!empty($otpClaim->getScope()) && !$otp->allowScope($otpClaim->getScope()))
|
|
throw new InvalidOTPException("OTP 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.");
|
|
}
|
|
|
|
$user = $this->getUserByUsername($otp->getUserName());
|
|
|
|
if (is_null($user)) {
|
|
// we need to create a new one ( auto register)
|
|
|
|
Log::debug(sprintf("AuthService::loginWithOTP user %s does not exists ...", $otp->getUserName()));
|
|
|
|
$user = $this->auth_user_service->registerUser([
|
|
'email' => $otp->getUserName(),
|
|
'email_verified' => true,
|
|
]);
|
|
}
|
|
|
|
$otp->setAuthTime(time());
|
|
$otp->setUserId($user->getId());
|
|
$otp->redeem();
|
|
|
|
Auth::login($user, false);
|
|
|
|
return $otp;
|
|
});
|
|
}
|
|
|
|
public function logout()
|
|
{
|
|
$this->invalidateSession();
|
|
Auth::logout();
|
|
$this->principal_service->clear();
|
|
// put in past
|
|
Cookie::queue
|
|
(
|
|
IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME,
|
|
null,
|
|
$minutes = -2628000,
|
|
$path = Config::get("session.path"),
|
|
$domain = Config::get("session.domain"),
|
|
$secure = true,
|
|
$httpOnly = true,
|
|
$raw = false,
|
|
$sameSite = 'none'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getUserAuthorizationResponse()
|
|
{
|
|
if (Session::has("openid.authorization.response")) {
|
|
$value = Session::get("openid.authorization.response");
|
|
|
|
return $value;
|
|
}
|
|
|
|
return IAuthService::AuthorizationResponse_None;
|
|
}
|
|
|
|
|
|
public function clearUserAuthorizationResponse()
|
|
{
|
|
if (Session::has("openid.authorization.response")) {
|
|
Session::remove("openid.authorization.response");
|
|
Session::save();
|
|
}
|
|
}
|
|
|
|
public function setUserAuthorizationResponse($auth_response)
|
|
{
|
|
Session::put("openid.authorization.response", $auth_response);
|
|
Session::save();
|
|
}
|
|
|
|
/**
|
|
* @param string $openid
|
|
* @return User|null
|
|
*/
|
|
public function getUserByOpenId(string $openid): ?User
|
|
{
|
|
return $this->user_repository->getByIdentifier($openid);
|
|
}
|
|
|
|
/**
|
|
* @param string $username
|
|
* @return null|User
|
|
*/
|
|
public function getUserByUsername(string $username): ?User
|
|
{
|
|
return $this->user_repository->getByEmailOrName($username);
|
|
}
|
|
|
|
/**
|
|
* @param int $id
|
|
* @return null|User
|
|
*/
|
|
public function getUserById(int $id): ?User
|
|
{
|
|
return $this->user_repository->getById($id);
|
|
}
|
|
|
|
// Authentication
|
|
|
|
public function getUserAuthenticationResponse()
|
|
{
|
|
if (Session::has("openstackid.authentication.response")) {
|
|
$value = Session::get("openstackid.authentication.response");
|
|
return $value;
|
|
}
|
|
return IAuthService::AuthenticationResponse_None;
|
|
}
|
|
|
|
public function setUserAuthenticationResponse($auth_response)
|
|
{
|
|
Session::put("openstackid.authentication.response", $auth_response);
|
|
Session::save();
|
|
}
|
|
|
|
public function clearUserAuthenticationResponse()
|
|
{
|
|
if (Session::has("openstackid.authentication.response")) {
|
|
Session::remove("openstackid.authentication.response");
|
|
Session::save();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $user_id
|
|
* @return string
|
|
*/
|
|
public function unwrapUserId(string $user_id): string
|
|
{
|
|
$user = $this->getUserById(intval($user_id));
|
|
|
|
if (!is_null($user))
|
|
return $user_id;
|
|
|
|
$unwrapped_name = $this->decrypt($user_id);
|
|
$parts = explode(':', $unwrapped_name);
|
|
return intval($parts[1]);
|
|
}
|
|
|
|
/**
|
|
* @param int $user_id
|
|
* @param IClient $client
|
|
* @return string
|
|
*/
|
|
public function wrapUserId(int $user_id, IClient $client): string
|
|
{
|
|
if ($client->getSubjectType() === IClient::SubjectType_Public)
|
|
return $user_id;
|
|
|
|
$wrapped_name = sprintf('%s:%s', $client->getClientId(), $user_id);
|
|
return $this->encrypt($wrapped_name);
|
|
}
|
|
|
|
/**
|
|
* @param string $value
|
|
* @return String
|
|
*/
|
|
private function encrypt(string $value): string
|
|
{
|
|
return base64_encode(Crypt::encrypt($value));
|
|
}
|
|
|
|
/**
|
|
* @param string $value
|
|
* @return String
|
|
*/
|
|
private function decrypt(string $value): string
|
|
{
|
|
$value = base64_decode($value);
|
|
return Crypt::decrypt($value);
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getSessionId(): string
|
|
{
|
|
return Session::getId();
|
|
}
|
|
|
|
/**
|
|
* @param $client_id
|
|
* @return void
|
|
*/
|
|
public function registerRPLogin(string $client_id): void
|
|
{
|
|
|
|
try {
|
|
$rps = Cookie::get(IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME, "");
|
|
$zlib = CompressionAlgorithms_Registry::getInstance()->get(CompressionAlgorithmsNames::ZLib);
|
|
|
|
if (!empty($rps)) {
|
|
$rps = $this->decrypt($rps);
|
|
$rps = $zlib->uncompress($rps);
|
|
$rps .= '|';
|
|
}
|
|
if (is_null($rps)) $rps = "";
|
|
|
|
if (!str_contains($rps, $client_id))
|
|
$rps .= $client_id;
|
|
|
|
$rps = $zlib->compress($rps);
|
|
$rps = $this->encrypt($rps);
|
|
} catch (Exception $ex) {
|
|
Log::warning($ex);
|
|
$rps = "";
|
|
}
|
|
|
|
Cookie::queue
|
|
(
|
|
IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME,
|
|
$rps,
|
|
Config::get("session.lifetime", 120),
|
|
$path = Config::get("session.path"),
|
|
$domain = Config::get("session.domain"),
|
|
$secure = true,
|
|
$httpOnly = true,
|
|
$raw = false,
|
|
$sameSite = 'none'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getLoggedRPs(): array
|
|
{
|
|
$rps = Cookie::get(IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME);
|
|
$zlib = CompressionAlgorithms_Registry::getInstance()->get(CompressionAlgorithmsNames::ZLib);
|
|
|
|
if (!empty($rps)) {
|
|
$rps = $this->decrypt($rps);
|
|
$rps = $zlib->uncompress($rps);
|
|
return explode('|', $rps);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param string $jti
|
|
* @throws Exception
|
|
*/
|
|
public function reloadSession(string $jti): void
|
|
{
|
|
Log::debug(sprintf("AuthService::reloadSession jti %s", $jti));
|
|
$session_id = $this->cache_service->getSingleValue($jti);
|
|
|
|
Log::debug(sprintf("AuthService::reloadSession session_id %s", $session_id));
|
|
if (empty($session_id))
|
|
throw new ReloadSessionException('session not found!');
|
|
|
|
if ($this->cache_service->exists($session_id . "invalid")) {
|
|
// session was marked as void, check if we are authenticated
|
|
if (!Auth::check())
|
|
throw new ReloadSessionException('user not found!');
|
|
}
|
|
|
|
Session::setId(Crypt::decrypt($session_id));
|
|
Session::start();
|
|
if (!Auth::check()) {
|
|
$user_id = $this->principal_service->get()->getUserId();
|
|
Log::debug(sprintf("AuthService::reloadSession user_id %s", $user_id));
|
|
$user = $this->getUserById($user_id);
|
|
if (is_null($user))
|
|
throw new ReloadSessionException('user not found!');
|
|
Auth::login($user);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $client_id
|
|
* @param int $id_token_lifetime
|
|
* @return string
|
|
*/
|
|
public function generateJTI(string $client_id, int $id_token_lifetime): string
|
|
{
|
|
$session_id = Crypt::encrypt(Session::getId());
|
|
$encoder = new Base64UrlRepresentation();
|
|
$jti = $encoder->encode(hash('sha512', $session_id . $client_id, true));
|
|
|
|
$this->cache_service->addSingleValue($jti, $session_id, $id_token_lifetime);
|
|
|
|
return $jti;
|
|
}
|
|
|
|
public function invalidateSession(): void
|
|
{
|
|
$session_id = Crypt::encrypt(Session::getId());
|
|
$this->cache_service->addSingleValue($session_id . "invalid", $session_id);
|
|
}
|
|
|
|
} |