Passwordlress Flow

* API endpoints for embedded login flow
* unit tests
* UI Tweaks
* Universal login implementation

Signed-off-by: smarcet@gmail.com <smarcet@gmail.com>
Change-Id: Ib09f1486f5d9419ee1df64a9d1c41dc7c9a4a65c
Depends-on: https://review.opendev.org/c/osf/openstackid/+/791306
This commit is contained in:
smarcet@gmail.com 2021-06-16 17:48:25 -03:00
parent 87f8920dd5
commit c7c91ff3f1
99 changed files with 4461 additions and 775 deletions

View File

@ -385,7 +385,7 @@ class AdminController extends Controller {
if(is_null($endpoint)) return Response::view('errors.404', [], 404);
$user = $this->auth_service->getCurrentUser();
$selected_scopes = [];
$list = $endpoint->getScopes();
$list = $endpoint->getScope();
foreach($list as $selected_scope){
$selected_scopes[] = $selected_scope->getId();
}

View File

@ -675,6 +675,10 @@ final class ClientApiController extends APICRUDController
'id_token_encrypted_response_alg' => 'sometimes|required|encrypted_alg',
'id_token_encrypted_response_enc' => 'sometimes|required|encrypted_enc',
'admin_users' => 'nullable|int_array',
'pkce_enabled' => 'sometimes|boolean',
'otp_enabled' => 'sometimes|boolean',
'otp_length' => 'sometimes|integer|min:4|max:8',
'otp_lifetime' => 'sometimes|integer|min:60|max:600',
];
}

View File

@ -101,24 +101,32 @@ final class OAuth2ProviderController extends Controller
return $response;
} catch (OAuth2BaseException $ex1) {
$payload = [
'error' => $ex1->getError(),
'error_description' => $ex1->getMessage()
];
if (request()->isJson()) {
return Response::json($payload, 400);
}
return Response::view
(
'errors.400',
[
'error' => $ex1->getError(),
'error_description' => $ex1->getMessage()
],
$payload,
400
);
} catch (Exception $ex) {
Log::error($ex);
$payload = [
'error' => "Bad Request",
'error_description' => "Generic Error"
];
if (request()->isJson()) {
return Response::json($payload, 400);
}
return Response::view
(
'errors.400',
[
'error' => "Bad Request",
'error_description' => "Generic Error"
],
$payload,
400
);
}

View File

@ -28,6 +28,10 @@ use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
use Models\OAuth2\OAuth2OTP;
use OAuth2\Factories\OAuth2AuthorizationRequestFactory;
use OAuth2\OAuth2Message;
use OAuth2\OAuth2Protocol;
use OAuth2\Repositories\IApiScopeRepository;
use OAuth2\Repositories\IClientRepository;
use OpenId\Services\IUserService;
@ -170,7 +174,9 @@ final class UserController extends OpenIdController
$this->security_context_service = $security_context_service;
$this->middleware(function ($request, $next) {
Log::debug(sprintf("UserController::middleware"));
Log::debug(sprintf("UserController::middleware route %s %s", $request->getMethod(), $request->getRequestUri()));
if ($this->openid_memento_service->exists()) {
//openid stuff
Log::debug(sprintf("UserController::middleware OIDC"));
@ -232,16 +238,17 @@ final class UserController extends OpenIdController
/**
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAccount(){
public function getAccount()
{
try {
$email = Request::input("email", "");
if(empty($email)){
if (empty($email)) {
throw new ValidationException("empty email.");
}
$user = $this->auth_service->getUserByUsername($email);
if(is_null($user))
if (is_null($user))
throw new EntityNotFoundException();
return $this->ok(
@ -250,16 +257,83 @@ final class UserController extends OpenIdController
'full_name' => $user->getFullName()
]
);
}
catch (ValidationException $ex){
} catch (ValidationException $ex) {
Log::warning($ex);
return $this->error412($ex->getMessages());
}
catch (EntityNotFoundException $ex){
} catch (EntityNotFoundException $ex) {
Log::warning($ex);
return $this->error404();
} catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
catch (Exception $ex){
}
/**
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function emitOTP()
{
try {
$username = Request::input("username", "");
$connection = Request::input("connection", "");
$send = Request::input("send", "");
if (empty($username)) {
throw new ValidationException("empty username.");
}
if (empty($connection)) {
throw new ValidationException("empty username.");
}
if (empty($send)) {
throw new ValidationException("empty username.");
}
$client = null;
// check if we have a former oauth2 request
if ($this->oauth2_memento_service->exists()) {
Log::debug("UserController::getOTP exist a oauth auth request on session");
$oauth_auth_request = OAuth2AuthorizationRequestFactory::getInstance()->build
(
OAuth2Message::buildFromMemento($this->oauth2_memento_service->load())
);
if ($oauth_auth_request->isValid()) {
$client_id = $oauth_auth_request->getClientId();
$client = $this->client_repository->getClientById($client_id);
if (is_null($client))
throw new ValidationException("client does not exists");
$this->oauth2_memento_service->serialize($oauth_auth_request->getMessage()->createMemento());
}
}
$otp = $this->token_service->createOTPFromPayload([
OAuth2Protocol::OAuth2PasswordlessConnection => $connection,
OAuth2Protocol::OAuth2PasswordlessSend => $send,
OAuth2Protocol::OAuth2PasswordlessEmail => ($connection == OAuth2Protocol::OAuth2PasswordlessConnectionEmail) ? $username : null,
OAuth2Protocol::OAuth2PasswordlessPhoneNumber => ($connection == OAuth2Protocol::OAuth2PasswordlessConnectionSMS) ? $username : null
], $client);
return $this->created([
'otp_length' => $otp->getLength(),
'otp_lifetime' => $otp->getLifetime(),
]);
} catch (ValidationException $ex) {
Log::warning($ex);
return $this->error412($ex->getMessages());
} catch (EntityNotFoundException $ex) {
Log::warning($ex);
return $this->error404();
} catch (Exception $ex) {
Log::error($ex);
return $this->error500($ex);
}
@ -271,6 +345,7 @@ final class UserController extends OpenIdController
$login_attempts = 0;
$username = '';
$user = null;
try
{
@ -279,7 +354,7 @@ final class UserController extends OpenIdController
if (isset($data['username']))
$data['username'] = trim($data['username']);
if(isset($data['password']))
if (isset($data['password']))
$data['password'] = trim($data['password']);
$login_attempts = intval(Request::input('login_attempts'));
@ -287,52 +362,90 @@ final class UserController extends OpenIdController
$rules = [
'username' => 'required|email',
'password' => 'required',
'flow' => 'required|in:otp,password',
'connection' => 'sometimes|string|in:sms,email',
];
if ($login_attempts >= $max_login_attempts_2_show_captcha)
{
if ($login_attempts >= $max_login_attempts_2_show_captcha) {
$rules['g-recaptcha-response'] = 'required|recaptcha';
}
// Create a new validator instance.
$validator = Validator::make($data, $rules);
if ($validator->passes()) {
$username = $data['username'];
$password = $data['password'];
$flow = $data['flow'];
$remember = Request::input("remember");
$remember = !is_null($remember);
$connection = $data['connection'] ?? null;
if ($this->auth_service->login($username, $password, $remember))
{
return $this->login_strategy->postLogin();
try {
if ($flow == "password" && $this->auth_service->login($username, $password, $remember)) {
return $this->login_strategy->postLogin();
}
if ($flow == "otp") {
$client = null;
// check if we have a former oauth2 request
if ($this->oauth2_memento_service->exists()) {
Log::debug("UserController::getOTP exist a oauth auth request on session");
$oauth_auth_request = OAuth2AuthorizationRequestFactory::getInstance()->build
(
OAuth2Message::buildFromMemento($this->oauth2_memento_service->load())
);
if ($oauth_auth_request->isValid()) {
$client_id = $oauth_auth_request->getClientId();
$client = $this->client_repository->getClientById($client_id);
if (is_null($client))
throw new ValidationException("client does not exists");
$this->oauth2_memento_service->serialize($oauth_auth_request->getMessage()->createMemento());
}
}
$otpClaim = OAuth2OTP::fromParams($username, $connection, $password);
$this->auth_service->loginWithOTP($otpClaim, $client);
return $this->login_strategy->postLogin();
}
} catch (AuthenticationException $ex) {
// failed login attempt...
$user = $this->auth_service->getUserByUsername($username);
if (!is_null($user)) {
$login_attempts = $user->getLoginFailedAttempt();
}
return $this->login_strategy->errorLogin
(
[
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'error_message' => $ex->getMessage(),
'user_fullname' => !is_null($user) ? $user->getFullName() : "",
'user_pic' => !is_null($user) ? $user->getPic(): "",
'user_verified' => true,
'username' => $username,
'flow' => $flow
]
);
}
//failed login attempt...
$user = $this->auth_service->getUserByUsername($username);
if (!is_null($user)) {
$login_attempts = $user->getLoginFailedAttempt();
}
return $this->login_strategy->errorLogin
(
[
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'error_message' => "We are sorry, your username or password does not match an existing record.",
'user_fullname' => $user->getFullName(),
'user_pic' => $user->getPic(),
'user_verified' => true,
'username' => $username,
]
);
}
// validator errors
$response_data = [
'max_login_attempts_2_show_captcha' => $max_login_attempts_2_show_captcha,
'login_attempts' => $login_attempts,
'validator' => $validator
'validator' => $validator,
];
if(!is_null($user)){
@ -376,9 +489,14 @@ final class UserController extends OpenIdController
}
}
/**
* @return \Illuminate\Http\Response|mixed
*/
public function getConsent()
{
if (is_null($this->consent_strategy)) {
Log::error(sprintf("UserController::getConsent consent strategy is null. request %s %s", Request::method(), Request::path()));
return Response::view
(
'errors.400',
@ -389,10 +507,12 @@ final class UserController extends OpenIdController
400
);
}
return $this->consent_strategy->getConsent();
}
/**
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|mixed
*/
public function postConsent()
{
try {
@ -405,7 +525,9 @@ final class UserController extends OpenIdController
$validator = Validator::make($data, $rules);
if ($validator->passes()) {
if (is_null($this->consent_strategy)) {
Log::warning(sprintf("UserController::postConsent consent strategy is null"));
Log::error(sprintf("UserController::postConsent consent strategy is null. request %s %s", Request::method(), Request::path()));
return Response::view
(
'errors.400',
@ -463,7 +585,6 @@ final class UserController extends OpenIdController
$pic_url = str_contains($pic_url, 'http') ? $pic_url : $assets_url . $pic_url;
$params = [
'show_fullname' => $user->getShowProfileFullName(),
'username' => $user->getFullName(),
'show_email' => $user->getShowProfileEmail(),

View File

@ -67,7 +67,7 @@ class Kernel extends HttpKernel
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'ssl' => \App\Http\Middleware\SSLMiddleware::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class,
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
'oauth2.endpoint' => \App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator::class,
'oauth2.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdmin::class,

View File

@ -0,0 +1,76 @@
<?php namespace App\Mail;
/**
* Copyright 2021 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 Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
/**
* Class OAuth2PasswordlessOTPMail
* @package App\Mail
*/
class OAuth2PasswordlessOTPMail extends Mailable
{
use Queueable, SerializesModels;
public $tries = 3;
/**
* @var string
*/
public $otp;
/**
* @var string
*/
public $email;
/**
* @var int
*/
public $lifetime;
/**
* OAuth2PasswordlessOTPMail constructor.
* @param string $to
* @param string $otp
* @param int $lifetime
*/
public function __construct
(
string $to,
string $otp,
int $lifetime
)
{
$this->email = $to;
$this->otp = $otp;
$this->lifetime = $lifetime / 60;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$subject = sprintf("[%s] Your Verification Code", Config::get('app.app_name'));
Log::debug(sprintf("OAuth2PasswordlessOTPMail::build to %s", $this->email));
return $this->from(Config::get("mail.from"))
->to($this->email)
->subject($subject)
->view('emails.oauth2_passwordless_otp');
}
}

View File

@ -71,13 +71,13 @@ final class WelcomeNewUserEmail extends Mailable
public function build()
{
$subject = sprintf("%s email verification needed", Config::get('app.app_name'));
$subject = sprintf("Thank you for registering for an %s account", Config::get('app.app_name'));
$view = 'emails.welcome_new_user_email';
if(Config::get("app.tenant_name") == 'FNTECH') {
$view = 'emails.welcome_new_user_email_fn';
$subject = sprintf("Thank you for registering for an %s account", Config::get('app.app_name'));
}
Log::debug(sprintf("WelcomeNewUserEmail::build to %s", $this->user_email));
return $this->from(Config::get("mail.from"))
->to($this->user_email)

View File

@ -15,6 +15,7 @@
use App\libs\Utils\URLUtils;
use Auth\User;
use Doctrine\Common\Collections\Criteria;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry;
use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry;
@ -90,6 +91,24 @@ class Client extends BaseEntity implements IClient
*/
private $pkce_enabled;
/**
* @ORM\Column(name="otp_enabled", type="boolean")
* @var bool
*/
private $otp_enabled;
/**
* @ORM\Column(name="otp_length", type="integer")
* @var int
*/
private $otp_length;
/**
* @ORM\Column(name="otp_lifetime", type="integer")
* @var int
*/
private $otp_lifetime;
/**
* @ORM\Column(name="locked", type="boolean")
* @var bool
@ -343,13 +362,13 @@ class Client extends BaseEntity implements IClient
private $admin_users;
/**
* @ORM\OneToMany(targetEntity="Models\OAuth2\RefreshToken", mappedBy="client", cascade={"persist"}, orphanRemoval=true)
* @ORM\OneToMany(targetEntity="Models\OAuth2\RefreshToken", mappedBy="client", cascade={"persist", "remove"}, orphanRemoval=true)
* @var ArrayCollection
*/
private $refresh_tokens;
/**
* @ORM\OneToMany(targetEntity="Models\OAuth2\AccessToken", mappedBy="client", cascade={"persist"}, orphanRemoval=true)
* @ORM\OneToMany(targetEntity="Models\OAuth2\AccessToken", mappedBy="client", cascade={"persist", "remove"}, orphanRemoval=true)
* @var ArrayCollection
*/
private $access_tokens;
@ -364,6 +383,12 @@ class Client extends BaseEntity implements IClient
*/
private $scopes;
/**
* @ORM\OneToMany(targetEntity="Models\OAuth2\OAuth2OTP", mappedBy="client", cascade={"persist", "remove"}, orphanRemoval=true)
* @var ArrayCollection
*/
private $otp_grants;
/**
* Client constructor.
*/
@ -374,6 +399,7 @@ class Client extends BaseEntity implements IClient
$this->access_tokens = new ArrayCollection();
$this->refresh_tokens = new ArrayCollection();
$this->admin_users = new ArrayCollection();
$this->otp_grants = new ArrayCollection();
$this->scopes = new ArrayCollection();
$this->locked = false;
$this->active = false;
@ -399,6 +425,7 @@ class Client extends BaseEntity implements IClient
$this->max_refresh_token_issuance_basis = 0;
$this->max_refresh_token_issuance_qty = 0;
$this->pkce_enabled = false;
$this->otp_enabled = false;
}
public static $valid_app_types = [
@ -443,7 +470,9 @@ class Client extends BaseEntity implements IClient
$this->getApplicationType() == IClient::ApplicationType_Native ||
$this->getApplicationType() == IClient::ApplicationType_Web_App ||
// PCKE
$this->pkce_enabled;
$this->pkce_enabled ||
// Passwordless
$this->otp_enabled;
}
/**
@ -1603,4 +1632,107 @@ class Client extends BaseEntity implements IClient
}
$this->pkce_enabled = false;
}
/**
* @return bool
*/
public function isPasswordlessEnabled(): bool
{
return $this->otp_enabled;
}
public function enablePasswordless(): void
{
$this->otp_enabled = true;
$this->otp_length = intval(Config::get("otp.length"));
$this->otp_lifetime = intval(Config::get("otp.lifetime"));
}
public function disablePasswordless(): void
{
$this->otp_enabled = false;
}
/**
* @return int
*/
public function getOtpLength(): int
{
$res = $this->otp_length;
if(is_null($res)){
$res = intval(Config::get("otp.length"));
}
return $res;
}
/**
* @param int $otp_length
*/
public function setOtpLength(int $otp_length): void
{
$this->otp_length = $otp_length;
}
/**
* @return int
*/
public function getOtpLifetime(): int
{
$res = $this->otp_lifetime;
if(is_null($res)){
$res = intval(Config::get("otp.lifetime"));
}
return $res;
}
/**
* @param int $otp_lifetime
*/
public function setOtpLifetime(int $otp_lifetime): void
{
$this->otp_lifetime = $otp_lifetime;
}
public function getOTPGrantsByEmailNotRedeemed(string $email){
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('email', trim($email)));
$criteria->where(Criteria::expr()->isNull("redeemed_at"));
return $this->otp_grants->matching($criteria);
}
public function getOTPGrantsByPhoneNumberNotRedeemed(string $phone_number){
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('phone_number', trim($phone_number)));
$criteria->where(Criteria::expr()->isNull("redeemed_at"));
return $this->otp_grants->matching($criteria);
}
public function addOTPGrant(OAuth2OTP $otp){
if($this->otp_grants->contains($otp)) return;
$this->otp_grants->add($otp);
$otp->setClient($this);
}
public function removeOTPGrant(OAuth2OTP $otp){
if(!$this->otp_grants->contains($otp)) return;
$this->otp_grants->removeElement($otp);
$otp->clearClient();
}
public function getOTPByValue(string $value):?OAuth2OTP{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('value', trim($value)));
$res = $this->otp_grants->matching($criteria)->first();
return !$res ? null : $res;
}
/**
* @param string $value
* @return bool
*/
public function hasOTP(string $value):bool{
return !is_null($this->getOTPByValue($value));
}
}

View File

@ -14,6 +14,7 @@
use App\libs\Utils\URLUtils;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Models\OAuth2\Client;
use Models\OAuth2\ResourceServer;
use OAuth2\Models\IClient;
@ -199,14 +200,32 @@ final class ClientFactory
$client->disablePCKE();
}
if(isset($payload['otp_enabled'])) {
$otp_enabled = boolval($payload['otp_enabled']);
if($otp_enabled)
$client->enablePasswordless();
else
$client->disablePasswordless();
}
if(isset($payload['otp_length'])){
$client->setOtpLength(intval($payload['otp_length']));
}
if(isset($payload['otp_lifetime'])){
$client->setOtpLifetime(intval($payload['otp_lifetime']));
}
$scope_repository = App::make(IApiScopeRepository::class);
//add default scopes
foreach ($scope_repository->getDefaults() as $default_scope) {
Log::debug(sprintf("ClientFactory::populate processing scope %s", $default_scope->getName()));
if
(
$default_scope->getName() === OAuth2Protocol::OfflineAccess_Scope
&& !$client->canRequestRefreshTokens()
) {
Log::debug(sprintf("ClientFactory::populate skipping scope %s", $default_scope->getName()));
continue;
}
$client->addScope($default_scope);

View File

@ -0,0 +1,117 @@
<?php namespace App\Models\OAuth2\Factories;
/**
* Copyright 2021 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 Illuminate\Support\Facades\Config;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\OAuth2Protocol;
use OAuth2\Requests\OAuth2PasswordlessAuthenticationRequest;
use Utils\Services\IdentifierGenerator;
/**
* Class OTPFactory
* @package App\Models\OAuth2\Factories
*/
final class OTPFactory
{
/**
* @param OAuth2PasswordlessAuthenticationRequest $request
* @param IdentifierGenerator $identifier_generator
* @param Client|null $client
* @return OAuth2OTP
*/
static public function buildFromRequest
(
OAuth2PasswordlessAuthenticationRequest $request,
IdentifierGenerator $identifier_generator,
?Client $client = null
):OAuth2OTP{
$lifetime = Config::get("otp.lifetime", 120);
$length = Config::get("otp.length",6);
if(!is_null($client)){
$lifetime = $client->getOtpLifetime();
$length = $client->getOtpLength();
}
$otp = new OAuth2OTP($length, $lifetime);
$otp->setConnection($request->getConnection());
$otp->setSend($request->getSend());
$otp->setLifetime($lifetime);
$otp->setNonce($request->getNonce());
$otp->setRedirectUrl($request->getRedirectUri());
$otp->setScope($request->getScope());
$otp->setEmail($request->getEmail());
$otp->setPhoneNumber($request->getPhoneNumber());
$identifier_generator->generate($otp);
if(!is_null($client)){
// check that client does not has a value
while($client->hasOTP($otp->getValue())){
$identifier_generator->generate($otp);
}
// then add it
$client->addOTPGrant($otp);
}
return $otp;
}
/**
* @param array $payload
* @param IdentifierGenerator $identifier_generator
* @param Client|null $client
* @return OAuth2OTP
*/
static public function buildFromPayload
(
array $payload,
IdentifierGenerator $identifier_generator,
?Client $client = null
):OAuth2OTP{
$lifetime = Config::get("otp.lifetime", 120);
$length = Config::get("otp.length",6);
if(!is_null($client)){
$lifetime = $client->getOtpLifetime();
$length = $client->getOtpLength();
}
$otp = new OAuth2OTP($length, $lifetime);
$otp->setConnection($payload[OAuth2Protocol::OAuth2PasswordlessConnection]);
$otp->setSend($payload[OAuth2Protocol::OAuth2PasswordlessSend]);
$otp->setScope($payload[OAuth2Protocol::OAuth2Protocol_Scope] ?? null);
$otp->setLifetime($lifetime);
$otp->setNonce($payload[OAuth2Protocol::OAuth2Protocol_Nonce] ?? null);
$otp->setRedirectUrl($payload[OAuth2Protocol::OAuth2Protocol_RedirectUri] ?? null);
$otp->setEmail($payload[OAuth2Protocol::OAuth2PasswordlessEmail] ?? null);
$otp->setPhoneNumber($payload[OAuth2Protocol::OAuth2PasswordlessPhoneNumber] ?? null);
$identifier_generator->generate($otp);
if(!is_null($client)){
// check that client does not has a value
while($client->hasOTP($otp->getValue())){
$identifier_generator->generate($otp);
}
// then add it
$client->addOTPGrant($otp);
}
return $otp;
}
}

View File

@ -0,0 +1,467 @@
<?php namespace Models\OAuth2;
/**
* 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\Models\Utils\BaseEntity;
use Doctrine\ORM\Mapping AS ORM;
use DateTime;
use DateInterval;
use DateTimeZone;
use Illuminate\Support\Facades\Log;
use models\exceptions\ValidationException;
use OAuth2\OAuth2Protocol;
use OAuth2\Requests\OAuth2AccessTokenRequestPasswordless;
use Utils\IPHelper;
use Utils\Model\Identifier;
use Zend\Math\Rand;
/**
* @ORM\Entity(repositoryClass="App\Repositories\DoctrineOAuth2OTPRepository")
* @ORM\Table(name="oauth2_otp")
* Class OTP
* @package Models\OAuth2
*/
class OAuth2OTP extends BaseEntity implements Identifier
{
/**
* @ORM\Column(name="value", type="string")
* @var string
*/
private $value;
/**
* @ORM\Column(name="length", type="integer")
* @var int
*/
private $length;
/**
* @ORM\Column(name="`connection`", type="string")
* @var string
*/
private $connection;
/**
* @ORM\Column(name="send", type="string")
* @var string
*/
private $send;
/**
* @ORM\Column(name="scope", type="string")
* @var string
*/
private $scope;
/**
* @ORM\Column(name="email", type="string")
* @var string
*/
private $email;
/**
* @ORM\Column(name="phone_number", type="string")
* @var string
*/
private $phone_number;
/**
* @ORM\Column(name="nonce", type="string")
* @var string
*/
private $nonce;
/**
* @ORM\Column(name="lifetime", type="integer")
* @var int
*/
private $lifetime;
/**
* @ORM\Column(name="redirect_url", type="string")
* @var string
*/
private $redirect_url;
/**
* @var \DateTime
* @ORM\Column(name="redeemed_at", type="datetime")
*/
private $redeemed_at;
/**
* @var string
* @ORM\Column(name="redeemed_from_ip", type="string")
*/
private $redeemed_from_ip;
/**
* @ORM\Column(name="redeemed_attempts", type="integer")
* @var int
*/
private $redeemed_attempts;
/**
* @ORM\ManyToOne(targetEntity="Models\OAuth2\Client", inversedBy="otp_grants", cascade={"persist"})
* @ORM\JoinColumn(name="oauth2_client_id", referencedColumnName="id", nullable=true)
* @var Client
*/
private $client;
/**
* OAuth2OTP constructor.
* @param int $length
* @param int $lifetime
*/
public function __construct(int $length, int $lifetime = 0 )
{
parent::__construct();
$this->length = $length;
$this->lifetime = $lifetime;
$this->redeemed_at = null;
$this->redeemed_attempts = 0;
}
/**
* @return string
*/
public function getValue(): string
{
return $this->value;
}
/**
* @return string
*/
public function getConnection(): string
{
return $this->connection;
}
/**
* @param string $connection
*/
public function setConnection(string $connection): void
{
$this->connection = $connection;
}
/**
* @return string
*/
public function getSend(): string
{
return $this->send;
}
/**
* @param string $send
*/
public function setSend(string $send): void
{
$this->send = $send;
}
/**
* @return string
*/
public function getScope(): ?string
{
return $this->scope;
}
/**
* @param string $scope
*/
public function setScope(?string $scope): void
{
$this->scope = $scope;
}
/**
* @return string
*/
public function getEmail(): ?string
{
return $this->email;
}
/**
* @param string $email
*/
public function setEmail(?string $email): void
{
$this->email = $email;
}
/**
* @return string
*/
public function getPhoneNumber(): ?string
{
return $this->phone_number;
}
/**
* @param string $phone_number
*/
public function setPhoneNumber(?string $phone_number): void
{
$this->phone_number = $phone_number;
}
/**
* @return string
*/
public function getNonce(): ?string
{
return $this->nonce;
}
/**
* @param string $nonce
*/
public function setNonce(?string $nonce): void
{
$this->nonce = $nonce;
}
/**
* @return string
*/
public function getRedirectUrl(): ?string
{
return $this->redirect_url;
}
/**
* @param string $redirect_url
*/
public function setRedirectUrl(?string $redirect_url): void
{
$this->redirect_url = $redirect_url;
}
/**
* @return \DateTime|null
*/
public function getRedeemedAt(): ?\DateTime
{
return $this->redeemed_at;
}
public function isRedeemed():bool{
return !is_null($this->redeemed_at);
}
/**
* @throws ValidationException
*/
public function redeem(): void
{
if(!is_null($this->redeemed_at))
throw new ValidationException("OTP is already redeemed.");
$this->redeemed_at = new \DateTime('now', new \DateTimeZone('UTC'));
$this->redeemed_from_ip = IPHelper::getUserIp();
Log::debug(sprintf("OAuth2OTP::redeem from ip %s", $this->redeemed_from_ip));
}
/**
* @return Client
*/
public function getClient(): Client
{
return $this->client;
}
public function hasClient():bool{
return !is_null($this->client);
}
/**
* @param Client $client
*/
public function setClient(Client $client): void
{
$this->client = $client;
}
/**
* @return int
*/
public function getLifetime(): int
{
return $this->lifetime;
}
/**
* @param int $lifetime
*/
public function setLifetime(int $lifetime): void
{
$this->lifetime = $lifetime;
}
public function getRemainingLifetime()
{
//check is refresh token is stills alive... (ZERO is infinite lifetime)
if (intval($this->lifetime) == 0) {
return 0;
}
$created_at = clone $this->created_at;
$created_at->add(new DateInterval('PT' . intval($this->lifetime) . 'S'));
$now = new DateTime(gmdate("Y-m-d H:i:s", time()), new DateTimeZone("UTC"));
//check validity...
if ($now > $created_at) {
return -1;
}
$seconds = abs($created_at->getTimestamp() - $now->getTimestamp());;
return $seconds;
}
public function isAlive():bool{
return $this->getRemainingLifetime() >= 0;
}
public function clearClient():void{
$this->client = null;
}
/**
* @return int
*/
public function getLength(): int
{
return $this->length;
}
const MaxRedeemAttempts = 3;
public function logRedeemAttempt():void{
if($this->redeemed_attempts < self::MaxRedeemAttempts){
$this->redeemed_attempts = $this->redeemed_attempts + 1;
Log::debug(sprintf("OAuth2OTP::logRedeemAttempt redeemed_attempts %s", $this->redeemed_attempts));
}
}
public function isValid():bool{
return ($this->redeemed_attempts < self::MaxRedeemAttempts) && $this->isAlive();
}
public function getUserName():?string{
return $this->connection == OAuth2Protocol::OAuth2PasswordlessEmail ? $this->email : $this->phone_number;
}
/**
* @param string $scope
* @return bool
*/
public function allowScope(string $scope):bool{
$s1 = explode(" ", $scope);
$s2 = explode(" ", $this->scope);
return count(array_diff($s1, $s2)) == 0;
}
public function setValue(string $value)
{
$this->value = $value;
}
public function getType(): string
{
return "otp";
}
const VsChar = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public function generateValue(): string
{
// calculate value
// entropy(SHANNON FANO Approx) len * log(count(VsChar))/log(2) = bits of entropy
$this->value = Rand::getString($this->length, self::VsChar);
return $this->value;
}
/**
* @param OAuth2AccessTokenRequestPasswordless $request
* @param int $length
* @return OAuth2OTP
*/
public static function fromRequest(OAuth2AccessTokenRequestPasswordless $request, int $length):OAuth2OTP{
$instance = new self($length);
$instance->connection = $request->getConnection();
$instance->email = $request->getEmail();
$instance->phone_number = $request->getPhoneNumber();
$instance->scope = $request->getScopes();
$instance->value = $request->getOTP();
return $instance;
}
/**
* @param string $user_name
* @param string $connection
* @param string $value
* @return OAuth2OTP|null
*/
public static function fromParams(string $user_name, string $connection, string $value):?OAuth2OTP{
$instance = new self(strlen($value));
$instance->connection = $connection;
if($connection == OAuth2Protocol::OAuth2PasswordlessConnectionEmail)
$instance->email = $user_name;
if($connection == OAuth2Protocol::OAuth2PasswordlessConnectionEmail)
$instance->phone_number = $user_name;
$instance->value = $value;
return $instance;
}
// non db fields
private $auth_time;
private $user_id;
/**
* @param int $auth_time
*/
public function setAuthTime(int $auth_time): void
{
$this->auth_time = $auth_time;
}
/**
* @param mixed $user_id
*/
public function setUserId($user_id): void
{
$this->user_id = $user_id;
}
/**
* @return mixed
*/
public function getAuthTime()
{
return $this->auth_time;
}
/**
* @return mixed
*/
public function getUserId()
{
return $this->user_id;
}
}

View File

@ -12,7 +12,6 @@
* limitations under the License.
**/
use App\Models\Utils\BaseEntity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping AS ORM;
/**
* @ORM\Entity(repositoryClass="App\Repositories\DoctrineOAuth2TrailExceptionRepository")

View File

@ -13,7 +13,12 @@
**/
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
/**
* Class RouteServiceProvider
* @package App\Providers
@ -38,6 +43,28 @@ final class RouteServiceProvider extends ServiceProvider
public function boot()
{
parent::boot();
$this->configureRateLimiting();
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('account', function (Request $request) {
return Limit::perMinute(5)->by(optional($request->user())->id ?: $request->ip());
});
RateLimiter::for('otp', function (Request $request) {
return Limit::perMinute(10)->by(optional($request->user())->id ?: $request->ip());
});
RateLimiter::for('oauth2', function (Request $request) {
$maxAttempts = App::environment() == "testing" ? PHP_INT_MAX : 50;
return Limit::perMinute($maxAttempts)->by(optional($request->user())->id ?: $request->ip());
});
}
/**

View File

@ -0,0 +1,93 @@
<?php namespace App\Repositories;
/**
* Copyright 2019 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\Repositories\IOAuth2OTPRepository;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
/**
* Class DoctrineOAuth2OTPRepository
* @package App\Repositories
*/
class DoctrineOAuth2OTPRepository
extends ModelDoctrineRepository
implements IOAuth2OTPRepository
{
protected function getBaseEntity()
{
return OAuth2OTP::class;
}
public function getByValue(string $value): ?OAuth2OTP
{
return $this->findOneBy(['value' => trim($value)]);
}
/**
* @param string $connection
* @param string $user_name
* @param Client|null $client
* @return OAuth2OTP|null
*/
public function getByConnectionAndUserNameNotRedeemed
(
string $connection,
string $user_name,
?Client $client
):?OAuth2OTP
{
$query = $this->getEntityManager()
->createQueryBuilder()
->select("e")
->from($this->getBaseEntity(), "e")
->where("e.connection = (:connection)")
->andWhere("(e.email = (:user_name) or e.phone_number = (:user_name))")
->andWhere("e.redeemed_at is null")
->setParameter("connection", $connection)
->setParameter("user_name", $user_name);
// add client id condition
if(!is_null($client)){
$query->join("e.client", "c")->andWhere("c.id = :client_id")
->setParameter("client_id", $client->getId());
}
$query->addOrderBy("e.id", "DESC");
return $query->getQuery()->getOneOrNullResult();
}
/**
* @param string $user_name
* @param Client|null $client
* @return OAuth2OTP[]
*/
public function getByUserNameNotRedeemed
(
string $user_name,
?Client $client = null
)
{
$query = $this->getEntityManager()
->createQueryBuilder()
->select("e")
->from($this->getBaseEntity(), "e")
->andWhere("(e.email = (:user_name) or e.phone_number = (:user_name))")
->andWhere("e.redeemed_at is null")
->setParameter("user_name", $user_name);
// add client id condition
if(!is_null($client)){
$query->join("e.client", "c")->andWhere("c.id = :client_id")
->setParameter("client_id", $client->getId());
}
$query->addOrderBy("e.id", "DESC");
return $query->getQuery()->getResult();
}
}

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\libs\Auth\Models\SpamEstimatorFeed;
use App\libs\Auth\Models\UserRegistrationRequest;
use App\libs\Auth\Repositories\IBannedIPRepository;
@ -20,6 +21,7 @@ use App\libs\Auth\Repositories\IUserExceptionTrailRepository;
use App\libs\Auth\Repositories\IUserPasswordResetRequestRepository;
use App\libs\Auth\Repositories\IUserRegistrationRequestRepository;
use App\libs\Auth\Repositories\IWhiteListedIPRepository;
use App\libs\OAuth2\Repositories\IOAuth2OTPRepository;
use App\libs\OAuth2\Repositories\IOAuth2TrailExceptionRepository;
use App\Models\Repositories\IDisqusSSOProfileRepository;
use App\Models\Repositories\IRocketChatSSOProfileRepository;
@ -43,6 +45,7 @@ use Models\OAuth2\ApiScope;
use Models\OAuth2\ApiScopeGroup;
use Models\OAuth2\Client;
use Models\OAuth2\ClientPublicKey;
use Models\OAuth2\OAuth2OTP;
use Models\OAuth2\OAuth2TrailException;
use Models\OAuth2\RefreshToken;
use Models\OAuth2\ResourceServer;
@ -67,43 +70,45 @@ use OAuth2\Repositories\IServerPrivateKeyRepository;
use LaravelDoctrine\ORM\Facades\EntityManager;
use OpenId\Repositories\IOpenIdAssociationRepository;
use OpenId\Repositories\IOpenIdTrustedSiteRepository;
/**
* Class RepositoriesProvider
* @package Repositories
*/
final class RepositoriesProvider extends ServiceProvider implements DeferrableProvider
{
public function boot(){
public function boot()
{
}
public function register(){
public function register()
{
App::singleton(IGroupRepository::class,
function(){
function () {
return EntityManager::getRepository(Group::class);
}
);
App::singleton(IUserPasswordResetRequestRepository::class,
function(){
function () {
return EntityManager::getRepository(UserPasswordResetRequest::class);
})
;
});
App::singleton(IServerExtensionRepository::class,
function(){
function () {
return EntityManager::getRepository(ServerExtension::class);
}
);
App::singleton(IOpenIdTrustedSiteRepository::class,
function(){
function () {
return EntityManager::getRepository(OpenIdTrustedSite::class);
}
);
App::singleton(IOpenIdAssociationRepository::class,
function(){
function () {
return EntityManager::getRepository(OpenIdAssociation::class);
}
);
@ -111,171 +116,184 @@ final class RepositoriesProvider extends ServiceProvider implements DeferrablePr
// doctrine repos
App::singleton(IServerConfigurationRepository::class,
function(){
function () {
return EntityManager::getRepository(ServerConfiguration::class);
}
);
App::singleton(IUserExceptionTrailRepository::class,
function(){
function () {
return EntityManager::getRepository(UserExceptionTrail::class);
}
);
App::singleton(IBannedIPRepository::class,
function(){
function () {
return EntityManager::getRepository(BannedIP::class);
}
);
App::singleton(IWhiteListedIPRepository::class, function (){
App::singleton(IWhiteListedIPRepository::class, function () {
return EntityManager::getRepository(WhiteListedIP::class);
});
App::singleton(IUserRepository::class,
function(){
function () {
return EntityManager::getRepository(User::class);
}
);
App::singleton(
IResourceServerRepository::class,
function(){
function () {
return EntityManager::getRepository(ResourceServer::class);
}
);
App::singleton(
IApiRepository::class,
function(){
function () {
return EntityManager::getRepository(Api::class);
}
);
App::singleton(
IApiEndpointRepository::class,
function(){
function () {
return EntityManager::getRepository(ApiEndpoint::class);
}
);
App::singleton(
IClientRepository::class,
function(){
IClientRepository::class,
function () {
return EntityManager::getRepository(Client::class);
}
);
App::singleton(
IAccessTokenRepository::class,
function(){
function () {
return EntityManager::getRepository(AccessToken::class);
}
);
App::singleton(
IRefreshTokenRepository::class,
function(){
function () {
return EntityManager::getRepository(RefreshToken::class);
}
);
App::singleton(
IApiScopeRepository::class,
function(){
function () {
return EntityManager::getRepository(ApiScope::class);
}
);
App::singleton(
IApiScopeGroupRepository::class,
function(){
function () {
return EntityManager::getRepository(ApiScopeGroup::class);
}
);
App::singleton(
IOAuth2TrailExceptionRepository::class,
function(){
function () {
return EntityManager::getRepository(OAuth2TrailException::class);
}
);
App::singleton(
IClientPublicKeyRepository::class,
function(){
function () {
return EntityManager::getRepository(ClientPublicKey::class);
}
);
App::singleton(
IServerPrivateKeyRepository::class,
function(){
function () {
return EntityManager::getRepository(ServerPrivateKey::class);
}
);
App::singleton(
IUserRegistrationRequestRepository::class,
function(){
function () {
return EntityManager::getRepository(UserRegistrationRequest::class);
}
);
App::singleton(
ISpamEstimatorFeedRepository::class,
function(){
function () {
return EntityManager::getRepository(SpamEstimatorFeed::class);
}
);
App::singleton(
IDisqusSSOProfileRepository::class,
function(){
function () {
return EntityManager::getRepository(DisqusSSOProfile::class);
}
);
App::singleton(
IRocketChatSSOProfileRepository::class,
function(){
function () {
return EntityManager::getRepository(RocketChatSSOProfile::class);
}
);
App::singleton(
IStreamChatSSOProfileRepository::class,
function(){
function () {
return EntityManager::getRepository(StreamChatSSOProfile::class);
}
);
App::singleton(
IOAuth2OTPRepository::class,
function () {
return EntityManager::getRepository(OAuth2OTP::class);
}
);
}
public function provides()
{
return [
IServerConfigurationRepository::class,
IGroupRepository::class,
IOpenIdAssociationRepository::class,
IUserPasswordResetRequestRepository::class,
IServerExtensionRepository::class,
IOpenIdTrustedSiteRepository::class,
IOpenIdAssociationRepository::class,
IServerConfigurationRepository::class,
IUserExceptionTrailRepository::class,
IBannedIPRepository::class,
IWhiteListedIPRepository::class,
IUserRepository::class,
IResourceServerRepository::class,
IApiRepository::class,
IApiEndpointRepository::class,
IClientRepository::class,
IAccessTokenRepository::class,
IRefreshTokenRepository::class,
IApiScopeRepository::class,
IApiScopeGroupRepository::class,
IOAuth2TrailExceptionRepository::class,
IClientPublicKeyRepository::class,
IServerPrivateKeyRepository::class,
IClientRepository::class,
IApiScopeGroupRepository::class,
IApiEndpointRepository::class,
IRefreshTokenRepository::class,
IAccessTokenRepository::class,
IApiScopeRepository::class,
IApiRepository::class,
IResourceServerRepository::class,
IWhiteListedIPRepository::class,
IUserRegistrationRequestRepository::class,
ISpamEstimatorFeedRepository::class,
IDisqusSSOProfileRepository::class,
IRocketChatSSOProfileRepository::class,
IStreamChatSSOProfileRepository::class,
IOAuth2OTPRepository::class,
];
}
}

View File

@ -128,6 +128,7 @@ final class UserService extends AbstractService implements IUserService
if(count($default_groups) > 0){
$payload['groups'] = $default_groups;
}
$user = UserFactory::build($payload);
$this->user_repository->add($user);

View File

@ -273,7 +273,7 @@ final class ClientService extends AbstractService implements IClientService
return $this->tx_service->transaction(function () use ($id, $payload) {
$editing_user = $this->auth_service->getCurrentUser();
$editing_user = $this->auth_service->getCurrentUser();
$client = $this->client_repository->getById($id);

View File

@ -13,13 +13,13 @@
**/
use App\Http\Utils\IUserIPHelperProvider;
use App\libs\OAuth2\Repositories\IOAuth2OTPRepository;
use App\Services\Auth\IUserService;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use OAuth2\Services\AccessTokenGenerator;
use OAuth2\Services\AuthorizationCodeGenerator;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\OAuth2ServiceCatalog;
use OAuth2\Services\RefreshTokenGenerator;
use Utils\Services\IdentifierGenerator;
use Utils\Services\UtilsServiceCatalog;
use Illuminate\Support\Facades\App;
/**
@ -70,9 +70,7 @@ final class OAuth2ServiceProvider extends ServiceProvider implements DeferrableP
App::make(UtilsServiceCatalog::CacheService),
App::make(UtilsServiceCatalog::AuthenticationService),
App::make(OAuth2ServiceCatalog::UserConsentService),
new AuthorizationCodeGenerator(App::make(UtilsServiceCatalog::CacheService)),
new AccessTokenGenerator(App::make(UtilsServiceCatalog::CacheService)),
new RefreshTokenGenerator(App::make(UtilsServiceCatalog::CacheService)),
App::make(IdentifierGenerator::class),
App::make(\OAuth2\Repositories\IServerPrivateKeyRepository::class),
new HttpIClientJWKSetReader,
App::make(OAuth2ServiceCatalog::SecurityContextService),
@ -82,9 +80,11 @@ final class OAuth2ServiceProvider extends ServiceProvider implements DeferrableP
App::make(\OAuth2\Repositories\IAccessTokenRepository::class),
App::make(\OAuth2\Repositories\IRefreshTokenRepository::class),
App::make(\OAuth2\Repositories\IResourceServerRepository::class),
App::make(IOAuth2OTPRepository::class),
App::make(IUserIPHelperProvider::class),
App::make(IApiScopeService::class),
App::make(UtilsServiceCatalog::TransactionService)
App::make(IUserService::class),
App::make(UtilsServiceCatalog::TransactionService),
);
});
@ -95,6 +95,7 @@ final class OAuth2ServiceProvider extends ServiceProvider implements DeferrableP
public function provides()
{
return [
IdentifierGenerator::class,
\OAuth2\IResourceServerContext::class,
OAuth2ServiceCatalog::ClientCredentialGenerator,
OAuth2ServiceCatalog::ClientService,

View File

@ -14,7 +14,13 @@
use App\Http\Utils\IUserIPHelperProvider;
use App\libs\Auth\Models\IGroupSlugs;
use App\libs\OAuth2\Repositories\IOAuth2OTPRepository;
use App\Models\OAuth2\Factories\OTPFactory;
use App\Services\AbstractService;
use App\Services\Auth\IUserService;
use App\Strategies\OTP\OTPChannelStrategyFactory;
use App\Strategies\OTP\OTPTypeBuilderStrategyFactory;
use Auth\Exceptions\AuthenticationException;
use Auth\User;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
@ -23,6 +29,9 @@ use jwt\IBasicJWT;
use jwt\impl\JWTClaimSet;
use jwt\JWTClaim;
use models\exceptions\ValidationException;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\Exceptions\InvalidOTPException;
use OAuth2\Models\AccessToken;
use Models\OAuth2\AccessToken as AccessTokenDB;
use Models\OAuth2\RefreshToken as RefreshTokenDB;
@ -48,6 +57,7 @@ use OAuth2\Repositories\IRefreshTokenRepository;
use OAuth2\Repositories\IResourceServerRepository;
use OAuth2\Requests\OAuth2AuthenticationRequest;
use OAuth2\Requests\OAuth2AuthorizationRequest;
use OAuth2\Requests\OAuth2PasswordlessAuthenticationRequest;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\ITokenService;
use OAuth2\OAuth2Protocol;
@ -66,7 +76,7 @@ use Utils\Exceptions\UnacquiredLockException;
use utils\json_types\JsonValue;
use utils\json_types\NumericDate;
use utils\json_types\StringOrURI;
use Utils\Model\Identifier;
use Utils\Model\AbstractIdentifier;
use Utils\Services\IAuthService;
use Utils\Services\ICacheService;
use Utils\Services\IdentifierGenerator;
@ -100,6 +110,10 @@ final class TokenService extends AbstractService implements ITokenService
* @var IClientService
*/
private $client_service;
/**
* @var IUserService
*/
private $user_service;
/**
* @var ILockManagerService
*/
@ -123,15 +137,7 @@ final class TokenService extends AbstractService implements ITokenService
/**
* @var IdentifierGenerator
*/
private $auth_code_generator;
/**
* @var IdentifierGenerator
*/
private $access_token_generator;
/**
* @var IdentifierGenerator
*/
private $refresh_token_generator;
private $identifier_generator;
/**
* @var IServerPrivateKeyRepository
@ -187,6 +193,34 @@ final class TokenService extends AbstractService implements ITokenService
*/
private $ip_helper;
/**
* @var IOAuth2OTPRepository
*/
private $otp_repository;
/**
* TokenService constructor.
* @param IClientService $client_service
* @param ILockManagerService $lock_manager_service
* @param IServerConfigurationService $configuration_service
* @param ICacheService $cache_service
* @param IAuthService $auth_service
* @param IUserConsentService $user_consent_service
* @param IdentifierGenerator $identifier_generator
* @param IServerPrivateKeyRepository $server_private_key_repository
* @param IClientJWKSetReader $jwk_set_reader_service
* @param ISecurityContextService $security_context_service
* @param IPrincipalService $principal_service
* @param IdTokenBuilder $id_token_builder
* @param IClientRepository $client_repository
* @param IAccessTokenRepository $access_token_repository
* @param IRefreshTokenRepository $refresh_token_repository
* @param IResourceServerRepository $resource_server_repository
* @param IUserIPHelperProvider $ip_helper
* @param IApiScopeService $scope_service
* @param IUserService $user_service
* @param ITransactionService $tx_service
*/
public function __construct
(
IClientService $client_service,
@ -195,9 +229,7 @@ final class TokenService extends AbstractService implements ITokenService
ICacheService $cache_service,
IAuthService $auth_service,
IUserConsentService $user_consent_service,
IdentifierGenerator $auth_code_generator,
IdentifierGenerator $access_token_generator,
IdentifierGenerator $refresh_token_generator,
IdentifierGenerator $identifier_generator,
IServerPrivateKeyRepository $server_private_key_repository,
IClientJWKSetReader $jwk_set_reader_service,
ISecurityContextService $security_context_service,
@ -207,8 +239,10 @@ final class TokenService extends AbstractService implements ITokenService
IAccessTokenRepository $access_token_repository,
IRefreshTokenRepository $refresh_token_repository,
IResourceServerRepository $resource_server_repository,
IOAuth2OTPRepository $otp_repository,
IUserIPHelperProvider $ip_helper,
IApiScopeService $scope_service,
IUserService $user_service,
ITransactionService $tx_service
)
{
@ -220,9 +254,7 @@ final class TokenService extends AbstractService implements ITokenService
$this->cache_service = $cache_service;
$this->auth_service = $auth_service;
$this->user_consent_service = $user_consent_service;
$this->auth_code_generator = $auth_code_generator;
$this->access_token_generator = $access_token_generator;
$this->refresh_token_generator = $refresh_token_generator;
$this->identifier_generator = $identifier_generator;
$this->server_private_key_repository = $server_private_key_repository;
$this->jwk_set_reader_service = $jwk_set_reader_service;
$this->security_context_service = $security_context_service;
@ -234,6 +266,8 @@ final class TokenService extends AbstractService implements ITokenService
$this->resource_server_repository = $resource_server_repository;
$this->ip_helper = $ip_helper;
$this->scope_service = $scope_service;
$this->user_service = $user_service;
$this->otp_repository = $otp_repository;
Event::listen('oauth2.client.delete', function ($client_id) {
$this->revokeClientRelatedTokens($client_id);
@ -248,13 +282,13 @@ final class TokenService extends AbstractService implements ITokenService
* Creates a brand new authorization code
* @param OAuth2AuthorizationRequest $request
* @param bool $has_previous_user_consent
* @return Identifier
* @return AbstractIdentifier
*/
public function createAuthorizationCode
(
OAuth2AuthorizationRequest $request,
bool $has_previous_user_consent = false
): Identifier
): AbstractIdentifier
{
$user = $this->auth_service->getCurrentUser();
@ -276,7 +310,7 @@ final class TokenService extends AbstractService implements ITokenService
$prompt = $request->getPrompt(true);
}
$code = $this->auth_code_generator->generate
$code = $this->identifier_generator->generate
(
AuthorizationCode::create
(
@ -354,7 +388,7 @@ final class TokenService extends AbstractService implements ITokenService
public function createAccessToken(AuthorizationCode $auth_code, $redirect_uri = null)
{
$access_token = $this->access_token_generator->generate
$access_token = $this->identifier_generator->generate
(
AccessToken::create
(
@ -462,7 +496,7 @@ final class TokenService extends AbstractService implements ITokenService
public function createAccessTokenFromParams($client_id, $scope, $audience, $user_id = null)
{
$access_token = $this->access_token_generator->generate(AccessToken::createFromParams
$access_token = $this->identifier_generator->generate(AccessToken::createFromParams
(
$scope,
$client_id,
@ -559,7 +593,7 @@ final class TokenService extends AbstractService implements ITokenService
}
//create new access token
$access_token = $this->access_token_generator->generate
$access_token = $this->identifier_generator->generate
(
AccessToken::createFromRefreshToken
(
@ -830,7 +864,7 @@ final class TokenService extends AbstractService implements ITokenService
*/
public function createRefreshToken(AccessToken &$access_token, $refresh_cache = false)
{
$refresh_token = $this->refresh_token_generator->generate(
$refresh_token = $this->identifier_generator->generate(
RefreshToken::create(
$access_token,
$this->configuration_service->getConfigValue('OAuth2.RefreshToken.Lifetime')
@ -1485,4 +1519,146 @@ final class TokenService extends AbstractService implements ITokenService
return $this->getAccessToken($db_access_token->getValue(), true);
}
private function postCreateOTP(OAuth2OTP $otp,?Client $client):OAuth2OTP{
if(!is_null($client)){
// invalidate not redeemed former ones
$codes = $otp->getConnection() == OAuth2Protocol::OAuth2PasswordlessConnectionEmail ?
$client->getOTPGrantsByEmailNotRedeemed($otp->getUserName()):
$client->getOTPGrantsByPhoneNumberNotRedeemed($otp->getUserName());
foreach ($codes as $code){
if($code->getValue() == $otp->getValue()) continue;
$client->removeOTPGrant($code);
}
}
// create channel and value to send ( depending on connection and send params )
OTPChannelStrategyFactory::build($otp->getConnection())->send
(
OTPTypeBuilderStrategyFactory::build($otp->getSend()),
$otp
);
return $otp;
}
/**
* @param OAuth2PasswordlessAuthenticationRequest $request
* @param Client|null $client
* @return OAuth2OTP
* @throws Exception
*/
public function createOTPFromRequest(OAuth2PasswordlessAuthenticationRequest $request, ?Client $client):OAuth2OTP{
return $this->tx_service->transaction(function() use($request, $client){
return $this->postCreateOTP
(
OTPFactory::buildFromRequest($request, $this->identifier_generator, $client),
$client
);
});
}
/**
* @param array $payload
* @param Client|null $client
* @return OAuth2OTP
* @throws Exception
*/
public function createOTPFromPayload(array $payload, ?Client $client):OAuth2OTP{
return $this->tx_service->transaction(function() use($payload, $client){
$otp = $this->postCreateOTP
(
OTPFactory::buildFromPayload($payload, $this->identifier_generator, $client),
$client
);
if(is_null($client)){
foreach($this->otp_repository->getByUserNameNotRedeemed($otp->getUserName()) as $formerOTP){
$this->otp_repository->delete($formerOTP);
}
$this->otp_repository->add($otp);
}
return $otp;
});
}
/**
* @param OAuth2OTP $otp
* @param Client|null $client
* @return AccessToken
* @throws Exception
*/
public function createAccessTokenFromOTP(OAuth2OTP $otp, ?Client $client): AccessToken
{
try {
$otp = $this->auth_service->loginWithOTP($otp, $client);
// build current audience ...
$audience = $this->scope_service->getStrAudienceByScopeNames
(
explode
(
OAuth2Protocol::OAuth2Protocol_Scope_Delimiter,
$otp->getScope()
)
);
$access_token = $this->identifier_generator->generate
(
AccessToken::createFromOTP
(
$otp,
! is_null($client) ? $client->getClientId() : null,
$audience,
$this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime')
)
);
return $this->tx_service->transaction(function() use($access_token, $client){
// TODO; move to a factory
$value = $access_token->getValue();
$hashed_value = Hash::compute('sha256', $value);
$access_token_db = new AccessTokenDB();
$access_token_db->setValue($hashed_value);
$access_token_db->setFromIp($this->ip_helper->getCurrentUserIpAddress());
$access_token_db->setLifetime($access_token->getLifetime());
$access_token_db->setScope($access_token->getScope());
$access_token_db->setAudience($access_token->getAudience());
$access_token_db->setClient($client);
$access_token_db->setOwner($this->auth_service->getCurrentUser());
$this->access_token_repository->add($access_token_db);
//check if use refresh tokens...
if
(
$client->useRefreshToken() &&
$client->isPasswordlessEnabled() &&
str_contains($access_token->getScope(), OAuth2Protocol::OfflineAccess_Scope)
) {
Log::debug('TokenService::createAccessTokenFromOTP creating refresh token ...');
$this->createRefreshToken($access_token);
}
$this->storesAccessTokenOnCache($access_token);
// stores brand new access token hash value on a set by client id...
{
if (!is_null($client))
$this->cache_service->addMemberSet($client->getClientId() . TokenService::ClientAccessTokenPrefixList, $hashed_value);
$this->cache_service->incCounter
(
$client->getClientId() . TokenService::ClientAccessTokensQty,
TokenService::ClientAccessTokensQtyLifetime
);
}
return $access_token;
});
}
catch (AuthenticationException $ex){
throw new InvalidOTPException($ex->getMessage());
}
}
}

View File

@ -15,8 +15,8 @@
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use OpenId\Services\NonceUniqueIdentifierGenerator;
use OpenId\Services\OpenIdServiceCatalog;
use Utils\Services\IdentifierGenerator;
use Utils\Services\UtilsServiceCatalog;
/**
* Class OpenIdProvider
@ -44,7 +44,7 @@ final class OpenIdProvider extends ServiceProvider implements DeferrableProvider
App::make(UtilsServiceCatalog::LockManagerService),
App::make(UtilsServiceCatalog::CacheService),
App::make(UtilsServiceCatalog::ServerConfigurationService),
new NonceUniqueIdentifierGenerator(App::make(UtilsServiceCatalog::CacheService))
App::make(IdentifierGenerator::class),
);
});
}

View File

@ -15,6 +15,8 @@ use App\Models\Utils\BaseEntity;
use App\Repositories\IServerConfigurationRepository;
use App\Services\Utils\DoctrineTransactionService;
use Illuminate\Contracts\Support\DeferrableProvider;
use Utils\Services\IdentifierGenerator;
use Utils\Services\UniqueIdentifierGenerator;
use Utils\Services\UtilsServiceCatalog;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\App;
@ -31,6 +33,8 @@ final class UtilsProvider extends ServiceProvider implements DeferrableProvider
*/
public function register()
{
App::singleton(IdentifierGenerator::class, UniqueIdentifierGenerator::class);
App::singleton(UtilsServiceCatalog::CacheService, RedisCacheService::class);
App::singleton(UtilsServiceCatalog::TransactionService, function(){
return new DoctrineTransactionService(BaseEntity::EntityManager);
@ -55,12 +59,14 @@ final class UtilsProvider extends ServiceProvider implements DeferrableProvider
return new ExternalUrlService();
});
}
public function provides()
{
return
[
IdentifierGenerator::class,
UtilsServiceCatalog::CacheService,
UtilsServiceCatalog::TransactionService,
UtilsServiceCatalog::LogService,

View File

@ -0,0 +1,27 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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 Models\OAuth2\OAuth2OTP;
/**
* Interface IOTPChannelStrategy
* @package App\Strategies\OTP
*/
interface IOTPChannelStrategy
{
/**
* @param IOTPTypeBuilderStrategy $typeBuilderStrategy
* @param OAuth2OTP $otp
* @return bool
*/
public function send(IOTPTypeBuilderStrategy $typeBuilderStrategy, OAuth2OTP $otp):bool;
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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 Models\OAuth2\OAuth2OTP;
/**
* Interface IOTPTypeBuilderStrategy
* @package App\Strategies\OTP
*/
interface IOTPTypeBuilderStrategy
{
public function generate(OAuth2OTP $otp):string;
}

View File

@ -0,0 +1,53 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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\Mail\OAuth2PasswordlessOTPMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Models\OAuth2\OAuth2OTP;
/**
* Class OTPChannelEmailStrategy
* @package App\Strategies\OTP
*/
final class OTPChannelEmailStrategy
implements IOTPChannelStrategy
{
/**
* @param IOTPTypeBuilderStrategy $typeBuilderStrategy
* @param OAuth2OTP $otp
* @return bool
*/
public function send(IOTPTypeBuilderStrategy $typeBuilderStrategy, OAuth2OTP $otp): bool
{
$value = $typeBuilderStrategy->generate($otp);
// send email
try{
Mail::queue
(
new OAuth2PasswordlessOTPMail
(
$otp->getUserName(),
$value,
$otp->getLifetime()
)
);
}
catch (\Exception $ex){
Log::error($ex);
return false;
}
return true;
}
}

View File

@ -0,0 +1,31 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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 OAuth2\Exceptions\InvalidOAuth2Request;
use OAuth2\OAuth2Protocol;
/**
* Class OTPChannelStrategyFactory
* @package App\Strategies\OTP
*/
final class OTPChannelStrategyFactory
{
public static function build(string $connection):IOTPChannelStrategy{
switch($connection){
case OAuth2Protocol::OAuth2PasswordlessConnectionEmail:
return new OTPChannelEmailStrategy();
}
throw new InvalidOAuth2Request(sprintf("connection value %s is not valid", $connection));
}
}

View File

@ -0,0 +1,34 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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 OAuth2\Exceptions\InvalidOAuth2Request;
use OAuth2\OAuth2Protocol;
/**
* Class OTPTypeBuilderStrategyFactory
* @package App\Strategies\OTP
*/
final class OTPTypeBuilderStrategyFactory
{
/**
* @param string $send
* @return IOTPTypeBuilderStrategy
*/
public static function build(string $send):IOTPTypeBuilderStrategy{
switch($send){
case OAuth2Protocol::OAuth2PasswordlessSendCode:
return new OTPTypeCodeBuilderStrategy();
}
throw new InvalidOAuth2Request(sprintf("send value %s is not valid", $send));
}
}

View File

@ -0,0 +1,31 @@
<?php namespace App\Strategies\OTP;
/**
* Copyright 2021 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 Models\OAuth2\OAuth2OTP;
/**
* Class OTPTypeCodeBuilderStrategy
* @package App\Strategies\OTP
*/
final class OTPTypeCodeBuilderStrategy
implements IOTPTypeBuilderStrategy
{
/**
* @param OAuth2OTP $otp
* @return string
*/
public function generate(OAuth2OTP $otp): string
{
return $otp->getValue();
}
}

View File

@ -11,29 +11,38 @@
* 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\Models\IOpenIdUser;
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 implements IAuthService
final class AuthService extends AbstractService implements IAuthService
{
/**
* @var IPrincipalService
@ -53,25 +62,44 @@ final class AuthService implements IAuthService
*/
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
ICacheService $cache_service,
IAuthUserService $auth_user_service,
ITransactionService $tx_service
)
{
$this->user_repository = $user_repository;
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->user_service = $user_service;
$this->cache_service = $cache_service;
$this->auth_user_service = $auth_user_service;
$this->otp_repository = $otp_repository;
}
/**
@ -85,7 +113,7 @@ final class AuthService implements IAuthService
/**
* @return User|null
*/
public function getCurrentUser():?User
public function getCurrentUser(): ?User
{
return Auth::user();
}
@ -94,25 +122,96 @@ final class AuthService implements IAuthService
* @param string $username
* @param string $password
* @param bool $remember_me
* @return mixed
* @return bool
* @throws AuthenticationException
*/
public function login($username, $password, $remember_me)
public function login(string $username, string $password, bool $remember_me): bool
{
Log::debug("AuthService::login");
$res = Auth::attempt(['username' => $username, 'password' => $password], $remember_me);
if ($res)
{
Log::debug("AuthService::login: clearing principal");
$this->principal_service->clear();
$this->principal_service->register
(
$this->getCurrentUser()->getId(),
time()
);
$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.");
}
return $res;
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()
@ -140,8 +239,7 @@ final class AuthService implements IAuthService
*/
public function getUserAuthorizationResponse()
{
if (Session::has("openid.authorization.response"))
{
if (Session::has("openid.authorization.response")) {
$value = Session::get("openid.authorization.response");
return $value;
@ -153,8 +251,7 @@ final class AuthService implements IAuthService
public function clearUserAuthorizationResponse()
{
if (Session::has("openid.authorization.response"))
{
if (Session::has("openid.authorization.response")) {
Session::remove("openid.authorization.response");
Session::save();
}
@ -170,7 +267,7 @@ final class AuthService implements IAuthService
* @param string $openid
* @return User|null
*/
public function getUserByOpenId(string $openid):?User
public function getUserByOpenId(string $openid): ?User
{
return $this->user_repository->getByIdentifier($openid);
}
@ -179,7 +276,7 @@ final class AuthService implements IAuthService
* @param string $username
* @return null|User
*/
public function getUserByUsername(string $username):?User
public function getUserByUsername(string $username): ?User
{
return $this->user_repository->getByEmailOrName($username);
}
@ -188,7 +285,7 @@ final class AuthService implements IAuthService
* @param int $id
* @return null|User
*/
public function getUserById(int $id):?User
public function getUserById(int $id): ?User
{
return $this->user_repository->getById($id);
}
@ -212,26 +309,25 @@ final class AuthService implements IAuthService
public function clearUserAuthenticationResponse()
{
if (Session::has("openstackid.authentication.response"))
{
if (Session::has("openstackid.authentication.response")) {
Session::remove("openstackid.authentication.response");
Session::save();
}
}
/**
* @param string $user_id
* @param string $user_id
* @return string
*/
public function unwrapUserId(string $user_id):string
public function unwrapUserId(string $user_id): string
{
$user = $this->getUserById(intval($user_id));
if(!is_null($user))
if (!is_null($user))
return $user_id;
$unwrapped_name = $this->decrypt($user_id);
$parts = explode(':', $unwrapped_name);
$parts = explode(':', $unwrapped_name);
return intval($parts[1]);
}
@ -240,10 +336,10 @@ final class AuthService implements IAuthService
* @param IClient $client
* @return string
*/
public function wrapUserId(int $user_id, IClient $client):string
public function wrapUserId(int $user_id, IClient $client): string
{
if($client->getSubjectType() === IClient::SubjectType_Public)
return $user_id;
if ($client->getSubjectType() === IClient::SubjectType_Public)
return $user_id;
$wrapped_name = sprintf('%s:%s', $client->getClientId(), $user_id);
return $this->encrypt($wrapped_name);
@ -253,7 +349,7 @@ final class AuthService implements IAuthService
* @param string $value
* @return String
*/
private function encrypt(string $value):string
private function encrypt(string $value): string
{
return base64_encode(Crypt::encrypt($value));
}
@ -262,7 +358,7 @@ final class AuthService implements IAuthService
* @param string $value
* @return String
*/
private function decrypt(string $value):string
private function decrypt(string $value): string
{
$value = base64_decode($value);
return Crypt::decrypt($value);
@ -271,16 +367,16 @@ final class AuthService implements IAuthService
/**
* @return string
*/
public function getSessionId():string
public function getSessionId(): string
{
return Session::getId();
return Session::getId();
}
/**
* @param $client_id
* @return void
*/
public function registerRPLogin(string $client_id):void
public function registerRPLogin(string $client_id): void
{
try {
@ -292,15 +388,14 @@ final class AuthService implements IAuthService
$rps = $zlib->uncompress($rps);
$rps .= '|';
}
if(is_null($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){
} catch (Exception $ex) {
Log::warning($ex);
$rps = "";
}
@ -322,13 +417,12 @@ final class AuthService implements IAuthService
/**
* @return string[]
*/
public function getLoggedRPs():array
public function getLoggedRPs(): array
{
$rps = Cookie::get(IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME);
$rps = Cookie::get(IAuthService::LOGGED_RELAYING_PARTIES_COOKIE_NAME);
$zlib = CompressionAlgorithms_Registry::getInstance()->get(CompressionAlgorithmsNames::ZLib);
if(!empty($rps))
{
if (!empty($rps)) {
$rps = $this->decrypt($rps);
$rps = $zlib->uncompress($rps);
return explode('|', $rps);
@ -340,29 +434,28 @@ final class AuthService implements IAuthService
* @param string $jti
* @throws Exception
*/
public function reloadSession(string $jti):void
public function reloadSession(string $jti): void
{
Log::debug(sprintf("AuthService::reloadSession jti %s", $jti ));
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))
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")){
if ($this->cache_service->exists($session_id . "invalid")) {
// session was marked as void, check if we are authenticated
if(!Auth::check())
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))
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);
}
@ -373,19 +466,21 @@ final class AuthService implements IAuthService
* @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));
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);
public function invalidateSession(): void
{
$session_id = Crypt::encrypt(Session::getId());
$this->cache_service->addSingleValue($session_id . "invalid", $session_id);
}
}

View File

@ -21,7 +21,6 @@ class AuthenticationException extends Exception
public function __construct($message = "")
{
$message = "Authentication Exception : " . $message;
parent::__construct($message, 0, null);
}
}

View File

@ -474,14 +474,18 @@ class User extends BaseEntity
return $this->identifier;
}
public function getEmail()
public function getEmail():string
{
return $this->email;
}
/**
* @return string|null
*/
public function getFullName(): ?string
{
return $this->getFirstName() . " " . $this->getLastName();
$full_name = $this->getFirstName() . " " . $this->getLastName();
return !empty(trim($full_name)) ? $full_name : $this->email;
}
public function getFirstName()

View File

@ -0,0 +1,28 @@
<?php namespace OAuth2\Exceptions;
/**
* Copyright 2021 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 OAuth2\OAuth2Protocol;
/**
* Class InvalidOTPException
* @package App\libs\OAuth2\Exceptions
*/
class InvalidOTPException extends OAuth2BaseException
{
/**
* @return string
*/
public function getError()
{
return OAuth2Protocol::OAuth2Protocol_Error_InvalidOTP;
}
}

View File

@ -1,5 +1,4 @@
<?php namespace OAuth2\Services;
<?php namespace OAuth2\Exceptions;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -12,24 +11,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\OAuth2Protocol;
use Utils\Model\Identifier;
use Utils\Services\UniqueIdentifierGenerator;
use Zend\Math\Rand;
/**
* Class OAuth2TokenGenerator
* @package OAuth2\Services
* Class InvalidRedeemOTPException
* @package OAuth2\Exceptions
*/
class OAuth2TokenGenerator extends UniqueIdentifierGenerator
final class InvalidRedeemOTPException extends OAuth2BaseException
{
/**
* @param Identifier $identifier
* @return Identifier
* @return string
*/
protected function _generate(Identifier $identifier)
public function getError()
{
return $identifier->setValue(Rand::getString($identifier->getLenght(), OAuth2Protocol::VsChar, true));
return OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant;
}
}

View File

@ -11,6 +11,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\Requests\OAuth2PasswordlessAuthenticationRequest;
use OAuth2\Exceptions\InvalidAuthenticationRequestException;
use OAuth2\Exceptions\InvalidAuthorizationRequestException;
use OAuth2\OAuth2Protocol;
@ -33,8 +35,14 @@ final class OAuth2AuthorizationRequestFactory
$auth_request = new OAuth2AuthorizationRequest($msg);
$scope = $auth_request->getScope();
$response_type = $auth_request->getResponseType();
if($response_type == OAuth2Protocol::OAuth2Protocol_ResponseType_OTP){
return new OAuth2PasswordlessAuthenticationRequest($auth_request);
}
if(!is_null($scope) && str_contains($scope, OAuth2Protocol::OpenIdConnect_Scope) ) {
$auth_request = new OAuth2AuthenticationRequest($auth_request);
return new OAuth2AuthenticationRequest($auth_request);
}
return $auth_request;

View File

@ -90,10 +90,10 @@ abstract class AbstractGrantType implements IGrantType
*/
public function completeFlow(OAuth2Request $request)
{
//get client credentials from request..
// get client credentials from request..
$this->client_auth_context = $this->client_service->getCurrentClientAuthInfo();
//retrieve client from storage..
// retrieve client from storage..
$this->current_client = $this->client_repository->getClientById($this->client_auth_context->getId());
if (is_null($this->current_client))

View File

@ -0,0 +1,379 @@
<?php namespace OAuth2\GrantTypes;
/**
* Copyright 2021 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 Exception;
use Illuminate\Support\Facades\Auth;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\Exceptions\InvalidApplicationType;
use OAuth2\Exceptions\InvalidClientException;
use OAuth2\Exceptions\InvalidOAuth2Request;
use OAuth2\Exceptions\InvalidOTPException;
use OAuth2\Exceptions\InvalidRedeemOTPException;
use OAuth2\Exceptions\LockedClientException;
use OAuth2\Exceptions\OAuth2BaseException;
use OAuth2\Exceptions\ScopeNotAllowedException;
use OAuth2\Exceptions\UnAuthorizedClientException;
use OAuth2\Exceptions\UriNotAllowedException;
use OAuth2\Models\IClient;
use OAuth2\OAuth2Protocol;
use OAuth2\Repositories\IClientRepository;
use OAuth2\Repositories\IServerPrivateKeyRepository;
use OAuth2\Requests\OAuth2AccessTokenRequestPasswordless;
use OAuth2\Requests\OAuth2AuthorizationRequest;
use OAuth2\Requests\OAuth2PasswordlessAuthenticationRequest;
use OAuth2\Requests\OAuth2Request;
use OAuth2\Requests\OAuth2TokenRequest;
use OAuth2\Responses\OAuth2AccessTokenResponse;
use OAuth2\Responses\OAuth2DirectErrorResponse;
use OAuth2\Responses\OAuth2IdTokenResponse;
use OAuth2\Responses\OAuth2PasswordlessAuthenticationResponse;
use OAuth2\Responses\OAuth2Response;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\IClientJWKSetReader;
use OAuth2\Services\IClientService;
use OAuth2\Services\IMementoOAuth2SerializerService;
use OAuth2\Services\IPrincipalService;
use OAuth2\Services\ISecurityContextService;
use OAuth2\Services\ITokenService;
use OAuth2\Services\IUserConsentService;
use OAuth2\Strategies\IOAuth2AuthenticationStrategy;
use Utils\Services\IAuthService;
use Utils\Services\ILogService;
/**
* Class PasswordlessGrantType
* @package OAuth2\GrantTypes
*/
class PasswordlessGrantType extends InteractiveGrantType
{
/**
* @var Client
*/
private $client = null;
/**
* PasswordlessGrantType constructor.
* @param IApiScopeService $scope_service
* @param IClientService $client_service
* @param IClientRepository $client_repository
* @param ITokenService $token_service
* @param IAuthService $auth_service
* @param IOAuth2AuthenticationStrategy $auth_strategy
* @param ILogService $log_service
* @param IUserConsentService $user_consent_service
* @param IMementoOAuth2SerializerService $memento_service
* @param ISecurityContextService $security_context_service
* @param IPrincipalService $principal_service
* @param IServerPrivateKeyRepository $server_private_key_repository
* @param IClientJWKSetReader $jwk_set_reader_service
*/
public function __construct
(
IApiScopeService $scope_service,
IClientService $client_service,
IClientRepository $client_repository,
ITokenService $token_service,
IAuthService $auth_service,
IOAuth2AuthenticationStrategy $auth_strategy,
ILogService $log_service,
IUserConsentService $user_consent_service,
IMementoOAuth2SerializerService $memento_service,
ISecurityContextService $security_context_service,
IPrincipalService $principal_service,
IServerPrivateKeyRepository $server_private_key_repository,
IClientJWKSetReader $jwk_set_reader_service
)
{
parent::__construct
(
$client_service,
$client_repository,
$token_service,
$log_service,
$security_context_service,
$principal_service,
$auth_service,
$user_consent_service,
$scope_service,
$auth_strategy,
$memento_service,
$server_private_key_repository,
$jwk_set_reader_service
);
}
/**
* @param OAuth2Request $request
* @return bool
*/
public function canHandle(OAuth2Request $request)
{
// 2 steps flow
// start flow
if
(
$request instanceof OAuth2PasswordlessAuthenticationRequest &&
OAuth2Protocol::responseTypeBelongsToFlow
(
$request->getResponseType(false),
OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless
)
) {
return true;
}
// complete flow
$request = $this->buildTokenRequest($request);
if
(
!is_null($request) &&
$request instanceof OAuth2AccessTokenRequestPasswordless &&
$request->getGrantType() == $this->getType()
) {
return true;
}
return false;
}
public function getType()
{
return OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless;
}
public function getResponseType()
{
return OAuth2Protocol::getValidResponseTypes(OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless);
}
protected function checkClientTypeAccess(IClient $client)
{
if (!$client->isPasswordlessEnabled()) {
throw new InvalidApplicationType
(
sprintf
(
"client id %s must have Passwordless enabled",
$client->getClientId(),
)
);
}
}
/**
* @param OAuth2AuthorizationRequest $request
* @param bool $has_former_consent
* @return OAuth2PasswordlessAuthenticationResponse|OAuth2Response
* @throws InvalidOAuth2Request
*/
protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent)
{
if (!($request instanceof OAuth2PasswordlessAuthenticationRequest)) {
throw new InvalidOAuth2Request;
}
$otp = $this->token_service->createOTPFromRequest($request, $this->client);
return new OAuth2PasswordlessAuthenticationResponse
(
$otp->getLength(),
$otp->getRemainingLifetime(),
$otp->getScope()
);
}
/**
* @param OAuth2Request $request
* @return OAuth2Response
* @throws \Exception
*/
public function handle(OAuth2Request $request)
{
try {
if (!($request instanceof OAuth2PasswordlessAuthenticationRequest)) {
throw new InvalidOAuth2Request;
}
if(!$request->isValid()){
throw new InvalidOAuth2Request($request->getLastValidationError());
}
$client_id = $request->getClientId();
$this->client = $this->client_repository->getClientById($client_id);
if (is_null($this->client)) {
throw new InvalidClientException
(
sprintf
(
"client_id %s does not exists!",
$client_id
)
);
}
if (!$this->client->isActive() || $this->client->isLocked()) {
throw new LockedClientException
(
sprintf
(
'client id %s is locked',
$client_id
)
);
}
$this->checkClientTypeAccess($this->client);
//check redirect uri
$redirect_uri = $request->getRedirectUri();
if (!empty($redirect_uri) && !$this->client->isUriAllowed($redirect_uri)) {
throw new UriNotAllowedException
(
$redirect_uri
);
}
//check requested scope
$scope = $request->getScope();
$this->log_service->debug_msg(sprintf("scope %s", $scope));
if (empty($scope) || !$this->client->isScopeAllowed($scope)) {
throw new ScopeNotAllowedException($scope);
}
$response = $this->buildResponse($request, false);
// clear save data ...
$this->auth_service->clearUserAuthorizationResponse();
$this->memento_service->forget();
return $response;
}
catch(OAuth2BaseException $ex){
$this->log_service->warning($ex);
// clear save data ...
$this->auth_service->clearUserAuthorizationResponse();
$this->memento_service->forget();
return new OAuth2DirectErrorResponse($ex->getError(), $ex->getMessage());
}
catch (Exception $ex) {
$this->log_service->error($ex);
// clear save data ...
$this->auth_service->clearUserAuthorizationResponse();
$this->memento_service->forget();
throw $ex;
}
}
/**
* Implements last request processing for Authorization code (Access Token Request processing)
* @see http://tools.ietf.org/html/rfc6749#section-4.1.3 and
* @see http://tools.ietf.org/html/rfc6749#section-4.1.4
* @param OAuth2Request $request
* @return OAuth2AccessTokenResponse
* @throws \Exception
* @throws InvalidClientException
* @throws UnAuthorizedClientException
* @throws UriNotAllowedException
*/
public function completeFlow(OAuth2Request $request)
{
try {
if (!($request instanceof OAuth2AccessTokenRequestPasswordless)) {
throw new InvalidOAuth2Request;
}
if(!$request->isValid()){
throw new InvalidOAuth2Request($request->getLastValidationError());
}
parent::completeFlow($request);
$this->client = $this->client_auth_context->getClient();
$this->checkClientTypeAccess($this->client);
$otp = OAuth2OTP::fromRequest($request, $this->client->getOtpLength());
$access_token = $this->token_service->createAccessTokenFromOTP
(
$otp,
$this->client
);
$this->principal_service->register
(
$otp->getUserId(),
$otp->getAuthTime()
);
$id_token = $this->token_service->createIdToken
(
$otp->getNonce(),
$this->client->getClientId(),
$access_token
);
$refresh_token = $access_token->getRefreshToken();
if (!is_null($access_token))
$refresh_token = $access_token->getRefreshToken();
$response = new OAuth2IdTokenResponse
(
is_null($access_token) ? null : $access_token->getValue(),
is_null($access_token) ? null : $access_token->getLifetime(),
is_null($id_token) ? null : $id_token->toCompactSerialization(),
is_null($refresh_token) ? null : $refresh_token->getValue()
);
$user = $this->auth_service->getUserByUsername($otp->getUserName());
// emmit login
Auth::login($user, false);
$this->security_context_service->clear();
return $response;
} catch (InvalidOTPException $ex) {
$this->log_service->error($ex);
$this->security_context_service->clear();
throw new InvalidRedeemOTPException
(
$ex->getMessage()
);
}
}
/**
* @param OAuth2Request $request
* @return OAuth2AccessTokenRequestPasswordless|OAuth2Response|null
*/
public function buildTokenRequest(OAuth2Request $request)
{
if ($request instanceof OAuth2TokenRequest)
{
if ($request->getGrantType() !== $this->getType())
{
return null;
}
return new OAuth2AccessTokenRequestPasswordless($request->getMessage());
}
return null;
}
}

View File

@ -11,6 +11,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Models\OAuth2\OAuth2OTP;
use OAuth2\OAuth2Protocol;
use Zend\Math\Rand;
/**
* Class AccessToken
* @see http://tools.ietf.org/html/rfc6749#section-1.4
@ -23,6 +26,11 @@ class AccessToken extends Token {
*/
private $auth_code;
/**
* @var OAuth2OTP
*/
private $otp;
/**
* @var RefreshToken
*/
@ -40,7 +48,7 @@ class AccessToken extends Token {
* @param int $lifetime
* @return AccessToken
*/
public static function create(AuthorizationCode $auth_code, $lifetime = 3600){
public static function create(AuthorizationCode $auth_code, $lifetime = 3600){
$instance = new self();
$instance->user_id = $auth_code->getUserId();
$instance->scope = $auth_code->getScope();
@ -53,6 +61,25 @@ class AccessToken extends Token {
return $instance;
}
/**
* @param OAuth2OTP $otp
* @param string $client_id
* @param string $audience
* @param int $lifetime
* @return AccessToken
*/
public static function createFromOTP(OAuth2OTP $otp,string $client_id, string $audience, $lifetime = 3600){
$instance = new self();
$instance->otp = $otp;
$instance->scope = $otp->getScope();
// client id (oauth2) not client identifier
$instance->client_id = $client_id;
$instance->audience = $audience;
$instance->lifetime = intval($lifetime);
$instance->is_hashed = false;
return $instance;
}
public static function createFromParams($scope, $client_id, $audience,$user_id,$lifetime){
$instance = new self();
$instance->scope = $scope;
@ -130,7 +157,7 @@ class AccessToken extends Token {
/**
* @return string
*/
public function getType()
public function getType():string
{
return 'access_token';
}
@ -142,4 +169,29 @@ class AccessToken extends Token {
{
return [];
}
/**
* @return OAuth2OTP
*/
public function getOtp(): ?OAuth2OTP
{
return $this->otp;
}
/**
* @return int
*/
public function getUserId()
{
if(!is_null($this->otp)){
$this->user_id = $this->otp->getUserId();
}
return intval($this->user_id);
}
public function generateValue(): string
{
$this->value = Rand::getString($this->len, OAuth2Protocol::VsChar);
return $this->value;
}
}

View File

@ -14,6 +14,7 @@
use Utils\IPHelper;
use OAuth2\OAuth2Protocol;
use Zend\Math\Rand;
/**
* Class AuthorizationCode
@ -313,7 +314,7 @@ class AuthorizationCode extends Token
/**
* @return string
*/
public function getType()
public function getType():string
{
return 'auth_code';
}
@ -384,4 +385,9 @@ class AuthorizationCode extends Token
return $this->code_challenge_method;
}
public function generateValue(): string
{
$this->value = Rand::getString($this->len, OAuth2Protocol::VsChar);
return $this->value;
}
}

View File

@ -324,4 +324,9 @@ interface IClient extends IEntity
* @return bool
*/
public function isPKCEEnabled():bool;
/**
* @return bool
*/
public function isPasswordlessEnabled():bool;
}

View File

@ -11,7 +11,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\OAuth2Protocol;
use Utils\IPHelper;
use Zend\Math\Rand;
/**
* Class RefreshToken
* @see http://tools.ietf.org/html/rfc6749#section-1.5
@ -81,7 +85,7 @@ class RefreshToken extends Token {
/**
* @return string
*/
public function getType()
public function getType():string
{
return 'refresh_token';
}
@ -93,4 +97,10 @@ class RefreshToken extends Token {
{
return [];
}
public function generateValue(): string
{
$this->value = Rand::getString($this->len, OAuth2Protocol::VsChar);
return $this->value;
}
}

View File

@ -15,13 +15,13 @@ use DateInterval;
use DateTime;
use DateTimeZone;
use Utils\IPHelper;
use Utils\Model\Identifier;
use Utils\Model\AbstractIdentifier;
/**
* Class Token
* Defines the common behavior for all emitted tokens
* @package OAuth2\Models
*/
abstract class Token extends Identifier
abstract class Token extends AbstractIdentifier
{
const DefaultByteLength = 32;
@ -54,6 +54,9 @@ abstract class Token extends Identifier
* @var bool
*/
protected $is_hashed;
/**
* @var
*/
protected $user_id;
public function __construct($len = self::DefaultByteLength)
@ -141,4 +144,5 @@ abstract class Token extends Identifier
public abstract function fromJSON($json);
}
}

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Http\Utils\UserIPHelperProvider;
use Exception;
use Illuminate\Support\Facades\Log;
@ -38,6 +39,7 @@ use OAuth2\GrantTypes\AuthorizationCodeGrantType;
use OAuth2\GrantTypes\ClientCredentialsGrantType;
use OAuth2\GrantTypes\HybridGrantType;
use OAuth2\GrantTypes\ImplicitGrantType;
use OAuth2\GrantTypes\PasswordlessGrantType;
use OAuth2\GrantTypes\RefreshBearerTokenGrantType;
use OAuth2\Models\IClient;
use OAuth2\Repositories\IClientRepository;
@ -64,6 +66,7 @@ use utils\factories\BasicJWTFactory;
use Utils\Services\IAuthService;
use Utils\Services\ICheckPointService;
use Utils\Services\ILogService;
/**
* Class OAuth2Protocol
* Implementation of @see http://tools.ietf.org/html/rfc6749
@ -76,21 +79,23 @@ final class OAuth2Protocol implements IOAuth2Protocol
* @var OAuth2Request
*/
private $last_request = null;
const OAuth2Protocol_Scope_Delimiter = ' ';
const OAuth2Protocol_Scope_Delimiter = ' ';
const OAuth2Protocol_ResponseType_Delimiter = ' ';
const OAuth2Protocol_GrantType_AuthCode = 'authorization_code';
const OAuth2Protocol_GrantType_Implicit = 'implicit';
const OAuth2Protocol_GrantType_Hybrid = 'hybrid';
const OAuth2Protocol_GrantType_AuthCode = 'authorization_code';
const OAuth2Protocol_GrantType_Passwordless = 'passwordless';
const OAuth2Protocol_GrantType_Implicit = 'implicit';
const OAuth2Protocol_GrantType_Hybrid = 'hybrid';
const OAuth2Protocol_GrantType_ResourceOwner_Password = 'password';
const OAuth2Protocol_GrantType_ClientCredentials = 'client_credentials';
const OAuth2Protocol_GrantType_RefreshToken = 'refresh_token';
const OAuth2Protocol_GrantType_ClientCredentials = 'client_credentials';
const OAuth2Protocol_GrantType_RefreshToken = 'refresh_token';
const OAuth2Protocol_ResponseType_Code = 'code';
const OAuth2Protocol_ResponseType_Token = 'token';
const OAuth2Protocol_ResponseType_Code = 'code';
const OAuth2Protocol_ResponseType_OTP = 'otp';
const OAuth2Protocol_ResponseType_Token = 'token';
const OAuth2Protocol_ResponseType_IdToken = 'id_token';
const OAuth2Protocol_ResponseType_None = 'none';
const OAuth2Protocol_ResponseType_None = 'none';
/**
* The OAuth 2.0 specification allows for registration of space-separated response_type parameter values. If a
@ -110,7 +115,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
* In this mode, Authorization Response parameters are encoded in the query string added to the redirect_uri when
* redirecting back to the Client.
*/
const OAuth2Protocol_ResponseMode_Query = 'query';
const OAuth2Protocol_ResponseMode_Query = 'query';
/**
* In this mode, Authorization Response parameters are encoded in the fragment added to the redirect_uri when
@ -127,9 +132,9 @@ final class OAuth2Protocol implements IOAuth2Protocol
* is intended to be used only once, the Authorization Server MUST instruct the User Agent (and any intermediaries)
* not to store or reuse the content of the response.
*/
const OAuth2Protocol_ResponseMode_FormPost = 'form_post';
const OAuth2Protocol_ResponseMode_FormPost = 'form_post';
const OAuth2Protocol_ResponseMode_Direct = 'direct';
const OAuth2Protocol_ResponseMode_Direct = 'direct';
static public $valid_response_modes = array
@ -155,21 +160,21 @@ final class OAuth2Protocol implements IOAuth2Protocol
static public function getDefaultResponseMode(array $response_type)
{
if(count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Code))) === 0)
if (count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Code))) === 0)
return self::OAuth2Protocol_ResponseMode_Query;
if(count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Token))) === 0)
if (count(array_diff($response_type, array(self::OAuth2Protocol_ResponseType_Token))) === 0)
return self::OAuth2Protocol_ResponseMode_Fragment;
// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations
if(count(array_diff($response_type, array
(
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_Token
)
if (count(array_diff($response_type, array
(
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_Token
)
)) === 0)
return self::OAuth2Protocol_ResponseMode_Fragment;
return self::OAuth2Protocol_ResponseMode_Fragment;
if(count(array_diff($response_type, array
if (count(array_diff($response_type, array
(
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_IdToken
@ -177,7 +182,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
)) === 0)
return self::OAuth2Protocol_ResponseMode_Fragment;
if(count(array_diff($response_type, array
if (count(array_diff($response_type, array
(
self::OAuth2Protocol_ResponseType_Token,
self::OAuth2Protocol_ResponseType_IdToken
@ -185,7 +190,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
)) === 0)
return self::OAuth2Protocol_ResponseMode_Fragment;
if(count(array_diff($response_type, array
if (count(array_diff($response_type, array
(
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_Token,
@ -197,21 +202,43 @@ final class OAuth2Protocol implements IOAuth2Protocol
}
const OAuth2Protocol_ClientId = 'client_id';
const OAuth2Protocol_UserId = 'user_id';
const OAuth2Protocol_ClientId = 'client_id';
const OAuth2Protocol_UserId = 'user_id';
const OAuth2Protocol_ClientSecret = 'client_secret';
const OAuth2Protocol_Token = 'token';
const OAuth2Protocol_TokenType = 'token_type';
const OAuth2Protocol_Token = 'token';
const OAuth2Protocol_TokenType = 'token_type';
// http://tools.ietf.org/html/rfc7009#section-2.1
const OAuth2Protocol_TokenType_Hint = 'token_type_hint';
const OAuth2Protocol_TokenType_Hint = 'token_type_hint';
const OAuth2Protocol_AccessToken_ExpiresIn = 'expires_in';
const OAuth2Protocol_RefreshToken = 'refresh_token';
const OAuth2Protocol_AccessToken = 'access_token';
const OAuth2Protocol_RedirectUri = 'redirect_uri';
const OAuth2Protocol_Scope = 'scope';
const OAuth2Protocol_Audience = 'audience';
const OAuth2Protocol_State = 'state';
const OAuth2Protocol_RefreshToken = 'refresh_token';
const OAuth2Protocol_AccessToken = 'access_token';
const OAuth2Protocol_RedirectUri = 'redirect_uri';
const OAuth2Protocol_Scope = 'scope';
const OAuth2Protocol_Audience = 'audience';
const OAuth2Protocol_State = 'state';
// passwordless
const OAuth2PasswordlessConnection = 'connection';
const OAuth2PasswordlessConnectionSMS = 'sms';
const OAuth2PasswordlessConnectionEmail = 'email';
const ValidOAuth2PasswordlessConnectionValues = [
self::OAuth2PasswordlessConnectionSMS,
self::OAuth2PasswordlessConnectionEmail
];
const OAuth2PasswordlessSend = 'send';
const OAuth2PasswordlessSendCode = 'code';
const OAuth2PasswordlessSendLink = 'link';
const ValidOAuth2PasswordlessSendValues = [
self::OAuth2PasswordlessSendCode,
self::OAuth2PasswordlessSendLink,
];
const OAuth2PasswordlessEmail = 'email';
const OAuth2PasswordlessPhoneNumber = 'phone_number';
/**
* @see http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
@ -223,13 +250,13 @@ final class OAuth2Protocol implements IOAuth2Protocol
* JSON string that represents the End-User's login state at the OP. It MUST NOT contain the space (" ") character.
* This value is opaque to the RP. This is REQUIRED if session management is supported.
*/
const OAuth2Protocol_Session_State = 'session_state';
const OAuth2Protocol_Session_State = 'session_state';
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
// ID Token value associated with the authenticated session.
const OAuth2Protocol_IdToken = 'id_token';
const OAuth2Protocol_IdToken = 'id_token';
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
const OAuth2Protocol_Nonce = 'nonce';
const OAuth2Protocol_Nonce = 'nonce';
/**
* custom param - social login
@ -242,7 +269,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
* is requested as an Essential Claim, then this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL.
* (The auth_time Claim semantically corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] auth_time response parameter.)
*/
const OAuth2Protocol_AuthTime = 'auth_time';
const OAuth2Protocol_AuthTime = 'auth_time';
/**
* Access Token hash value. Its value is the base64url encoding of the left-most half of the hash of the octets of
@ -267,35 +294,35 @@ final class OAuth2Protocol implements IOAuth2Protocol
* Specifies how the Authorization Server displays the authentication and consent user interface pages to
* the End-User.
*/
const OAuth2Protocol_Display ='display';
const OAuth2Protocol_Display = 'display';
/**
* The Authorization Server SHOULD display the authentication and consent UI consistent with a full User Agent page
* view. If the display parameter is not specified, this is the default display mode.
* The Authorization Server MAY also attempt to detect the capabilities of the User Agent and present an
* appropriate display.
*/
const OAuth2Protocol_Display_Page ='page';
const OAuth2Protocol_Display_Page = 'page';
/**
* The Authorization Server SHOULD display the authentication and consent UI consistent with a popup User Agent
* window. The popup User Agent window should be of an appropriate size for a login-focused dialog and should not
* obscure the entire window that it is popping up over.
*/
const OAuth2Protocol_Display_PopUp ='popup';
const OAuth2Protocol_Display_PopUp = 'popup';
/**
* The Authorization Server SHOULD display the authentication and consent UI consistent with a device that leverages
* a touch interface.
*/
const OAuth2Protocol_Display_Touch ='touch';
const OAuth2Protocol_Display_Touch = 'touch';
/**
* The Authorization Server SHOULD display the authentication and consent UI consistent with a "feature phone"
* type display.
*/
const OAuth2Protocol_Display_Wap ='wap';
const OAuth2Protocol_Display_Wap = 'wap';
/**
* Extension: display the login/consent interaction like a json doc
*/
const OAuth2Protocol_Display_Native ='native';
const OAuth2Protocol_Display_Native = 'native';
/**
* @var array
@ -363,14 +390,12 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
static public function getValidResponseTypes($flow = 'all')
{
$code_flow = array
(
$code_flow = [
//OAuth2 / OIDC
array
(
[
self::OAuth2Protocol_ResponseType_Code
)
);
]
];
$implicit_flow = array
(
@ -386,17 +411,17 @@ final class OAuth2Protocol implements IOAuth2Protocol
),
array
(
self::OAuth2Protocol_ResponseType_IdToken ,
self::OAuth2Protocol_ResponseType_IdToken,
self::OAuth2Protocol_ResponseType_Token
)
);
$hybrid_flow = array
$hybrid_flow = array
(
array
(
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_IdToken
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_IdToken
),
array
(
@ -405,13 +430,13 @@ final class OAuth2Protocol implements IOAuth2Protocol
),
array
(
self::OAuth2Protocol_ResponseType_Code ,
self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_IdToken,
self::OAuth2Protocol_ResponseType_Token
)
);
if($flow === 'all')
if ($flow === 'all')
return array_merge
(
$code_flow,
@ -419,13 +444,20 @@ final class OAuth2Protocol implements IOAuth2Protocol
$hybrid_flow
);
if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode)
if ($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless)
return [
[
self::OAuth2Protocol_ResponseType_OTP
]
];
if ($flow === OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode)
return $code_flow;
if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Implicit)
if ($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Implicit)
return $implicit_flow;
if($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid)
if ($flow === OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid)
return $hybrid_flow;
return [];
@ -446,26 +478,25 @@ final class OAuth2Protocol implements IOAuth2Protocol
{
if
(
!in_array
(
$flow, array
(
OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
OAuth2Protocol::OAuth2Protocol_GrantType_Implicit,
OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid,
'all'
)
)
!in_array
(
$flow, [
OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
OAuth2Protocol::OAuth2Protocol_GrantType_Implicit,
OAuth2Protocol::OAuth2Protocol_GrantType_Hybrid,
OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
'all'
]
)
return false;
)
return false;
$flow_response_types = self::getValidResponseTypes($flow);
foreach($flow_response_types as $rt)
{
if(count($rt) !== count($response_type)) continue;
$diff = array_diff($rt, $response_type);
if(count($diff) === 0) return true;
foreach ($flow_response_types as $rt) {
if (count($rt) !== count($response_type)) continue;
$diff = array_diff($rt, $response_type);
if (count($diff) === 0) return true;
}
return false;
}
@ -534,9 +565,9 @@ final class OAuth2Protocol implements IOAuth2Protocol
* through the sequence. If the value is force, then the user sees a consent page even if they
* previously gave consent to your application for a given set of scopes.
*/
const OAuth2Protocol_Approval_Prompt = 'approval_prompt';
const OAuth2Protocol_Approval_Prompt = 'approval_prompt';
const OAuth2Protocol_Approval_Prompt_Force = 'force';
const OAuth2Protocol_Approval_Prompt_Auto = 'auto';
const OAuth2Protocol_Approval_Prompt_Auto = 'auto';
/**
* Indicates whether your application needs to access an API when the user is not present at
@ -544,8 +575,8 @@ final class OAuth2Protocol implements IOAuth2Protocol
* when the user is not present at the browser, then use offline. This will result in your application
* obtaining a refresh token the first time your application exchanges an authorization code for a user.
*/
const OAuth2Protocol_AccessType = 'access_type';
const OAuth2Protocol_AccessType_Online = 'online';
const OAuth2Protocol_AccessType = 'access_type';
const OAuth2Protocol_AccessType_Online = 'online';
const OAuth2Protocol_AccessType_Offline = 'offline';
const OAuth2Protocol_GrantType = 'grant_type';
@ -554,6 +585,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
const OAuth2Protocol_ErrorUri = 'error_uri';
const OAuth2Protocol_Error_InvalidRequest = 'invalid_request';
const OAuth2Protocol_Error_UnauthorizedClient = 'unauthorized_client';
const OAuth2Protocol_Error_InvalidOTP = 'invalid_otp';
const OAuth2Protocol_Error_RedirectUriMisMatch = 'redirect_uri_mismatch';
const OAuth2Protocol_Error_AccessDenied = 'access_denied';
const OAuth2Protocol_Error_UnsupportedResponseType = 'unsupported_response_type';
@ -627,26 +659,26 @@ final class OAuth2Protocol implements IOAuth2Protocol
const OAuth2Protocol_Error_Invalid_Recipient_Keys = 'invalid_recipient_keys';
const OAuth2Protocol_Error_Invalid_Server_Keys = 'invalid_server_keys';
const OAuth2Protocol_Error_Not_Found_Server_Keys = 'not_found_server_keys';
const OAuth2Protocol_Error_Invalid_Server_Keys = 'invalid_server_keys';
const OAuth2Protocol_Error_Not_Found_Server_Keys = 'not_found_server_keys';
public static $valid_responses_types = array
(
self::OAuth2Protocol_ResponseType_Code => self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_Code => self::OAuth2Protocol_ResponseType_Code,
self::OAuth2Protocol_ResponseType_Token => self::OAuth2Protocol_ResponseType_Token
);
// http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
const TokenEndpoint_AuthMethod_ClientSecretBasic = 'client_secret_basic';
const TokenEndpoint_AuthMethod_ClientSecretPost = 'client_secret_post';
const TokenEndpoint_AuthMethod_ClientSecretJwt = 'client_secret_jwt';
const TokenEndpoint_AuthMethod_PrivateKeyJwt = 'private_key_jwt';
const TokenEndpoint_AuthMethod_None = 'none';
const TokenEndpoint_AuthMethod_ClientSecretPost = 'client_secret_post';
const TokenEndpoint_AuthMethod_ClientSecretJwt = 'client_secret_jwt';
const TokenEndpoint_AuthMethod_PrivateKeyJwt = 'private_key_jwt';
const TokenEndpoint_AuthMethod_None = 'none';
const OAuth2Protocol_ClientAssertionType = 'client_assertion_type';
const OAuth2Protocol_ClientAssertion = 'client_assertion';
const OAuth2Protocol_ClientAssertionType = 'client_assertion_type';
const OAuth2Protocol_ClientAssertion = 'client_assertion';
public static $token_endpoint_auth_methods = array
(
@ -721,8 +753,8 @@ final class OAuth2Protocol implements IOAuth2Protocol
/**
* PKCE
* @see https://tools.ietf.org/html/rfc7636
**/
* @see https://tools.ietf.org/html/rfc7636
**/
// auth request new params
@ -835,31 +867,48 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
public function __construct
(
ILogService $log_service,
ILogService $log_service,
IClientService $client_service,
IClientRepository $client_repository,
ITokenService $token_service,
IAuthService $auth_service,
ITokenService $token_service,
IAuthService $auth_service,
IOAuth2AuthenticationStrategy $auth_strategy,
ICheckPointService $checkpoint_service,
IApiScopeService $scope_service,
IApiScopeService $scope_service,
IUserConsentService $user_consent_service,
IServerPrivateKeyRepository $server_private_keys_repository,
IOpenIDProviderConfigurationService $oidc_provider_configuration_service,
IMementoOAuth2SerializerService $memento_service,
ISecurityContextService $security_context_service,
IPrincipalService $principal_service,
IServerPrivateKeyRepository $server_private_key_repository,
IClientJWKSetReader $jwk_set_reader_service,
UserIPHelperProvider $ip_helper
ISecurityContextService $security_context_service,
IPrincipalService $principal_service,
IServerPrivateKeyRepository $server_private_key_repository,
IClientJWKSetReader $jwk_set_reader_service,
UserIPHelperProvider $ip_helper
)
{
$this->server_private_keys_repository = $server_private_keys_repository;
$this->server_private_keys_repository = $server_private_keys_repository;
$this->oidc_provider_configuration_service = $oidc_provider_configuration_service;
$this->memento_service = $memento_service;
$this->memento_service = $memento_service;
$authorization_code_grant_type = new AuthorizationCodeGrantType
$passwordless_grant_type = new PasswordlessGrantType
(
$scope_service,
$client_service,
$client_repository,
$token_service,
$auth_service,
$auth_strategy,
$log_service,
$user_consent_service,
$this->memento_service,
$security_context_service,
$principal_service,
$server_private_key_repository,
$jwk_set_reader_service
);
$authorization_code_grant_type = new AuthorizationCodeGrantType
(
$scope_service,
$client_service,
@ -910,7 +959,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
$jwk_set_reader_service
);
$refresh_bearer_token_grant_type = new RefreshBearerTokenGrantType
$refresh_bearer_token_grant_type = new RefreshBearerTokenGrantType
(
$client_service,
$client_repository,
@ -918,7 +967,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
$log_service
);
$client_credential_grant_type = new ClientCredentialsGrantType
$client_credential_grant_type = new ClientCredentialsGrantType
(
$scope_service,
$client_service,
@ -927,24 +976,26 @@ final class OAuth2Protocol implements IOAuth2Protocol
$log_service
);
$this->grant_types[$authorization_code_grant_type->getType()] = $authorization_code_grant_type;
$this->grant_types[$implicit_grant_type->getType()] = $implicit_grant_type;
// setting grants collection
$this->grant_types[$passwordless_grant_type->getType()] = $passwordless_grant_type;
$this->grant_types[$authorization_code_grant_type->getType()] = $authorization_code_grant_type;
$this->grant_types[$implicit_grant_type->getType()] = $implicit_grant_type;
$this->grant_types[$refresh_bearer_token_grant_type->getType()] = $refresh_bearer_token_grant_type;
$this->grant_types[$client_credential_grant_type->getType()] = $client_credential_grant_type;
$this->grant_types[$hybrid_grant_type->getType()] = $hybrid_grant_type;
$this->grant_types[$client_credential_grant_type->getType()] = $client_credential_grant_type;
$this->grant_types[$hybrid_grant_type->getType()] = $hybrid_grant_type;
$this->log_service = $log_service;
$this->checkpoint_service = $checkpoint_service;
$this->client_service = $client_service;
$this->client_repository = $client_repository;
$this->auth_service = $auth_service;
$this->principal_service = $principal_service;
$this->token_service = $token_service;
$this->log_service = $log_service;
$this->checkpoint_service = $checkpoint_service;
$this->client_service = $client_service;
$this->client_repository = $client_repository;
$this->auth_service = $auth_service;
$this->principal_service = $principal_service;
$this->token_service = $token_service;
$this->authorize_endpoint = new AuthorizationEndpoint($this);
$this->token_endpoint = new TokenEndpoint($this);
$this->revoke_endpoint = new TokenRevocationEndpoint($this, $client_service, $client_repository, $token_service, $log_service);
$this->introspection_endpoint = new TokenIntrospectionEndpoint
$this->authorize_endpoint = new AuthorizationEndpoint($this);
$this->token_endpoint = new TokenEndpoint($this);
$this->revoke_endpoint = new TokenRevocationEndpoint($this, $client_service, $client_repository, $token_service, $log_service);
$this->introspection_endpoint = new TokenIntrospectionEndpoint
(
$this,
$client_service,
@ -964,14 +1015,12 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
public function authorize(OAuth2Request $request = null)
{
try
{
try {
$this->last_request = $request;
if (is_null($this->last_request)) throw new InvalidOAuth2Request;
if(!$this->last_request->isValid())
{
if (!$this->last_request->isValid()) {
// then check if we have a memento ....
if (!$this->memento_service->exists())
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
@ -981,20 +1030,16 @@ final class OAuth2Protocol implements IOAuth2Protocol
OAuth2Message::buildFromMemento($this->memento_service->load())
);
if(!$this->last_request->isValid())
if (!$this->last_request->isValid())
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
}
return $this->authorize_endpoint->handle($this->last_request);
}
catch (UriNotAllowedException $ex1)
{
} catch (UriNotAllowedException $ex1) {
$this->log_service->warning($ex1);
$this->checkpoint_service->trackException($ex1);
throw $ex1;
}
catch(OAuth2BaseException $ex2)
{
} catch (OAuth2BaseException $ex2) {
$this->log_service->warning($ex2);
$this->checkpoint_service->trackException($ex2);
@ -1010,8 +1055,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
$ex2->getMessage(),
$redirect_uri
);
}
catch (AbsentClientException $ex3){
} catch (AbsentClientException $ex3) {
$this->log_service->warning($ex3);
$this->checkpoint_service->trackException($ex3);
@ -1026,8 +1070,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
$ex3->getMessage(),
$redirect_uri
);
}
catch (AbsentCurrentUserException $ex4){
} catch (AbsentCurrentUserException $ex4) {
$this->log_service->warning($ex4);
$this->checkpoint_service->trackException($ex4);
@ -1042,9 +1085,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
$ex4->getMessage(),
$redirect_uri
);
}
catch (Exception $ex)
{
} catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);
@ -1068,27 +1109,22 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
public function token(OAuth2Request $request = null)
{
try
{
try {
$this->last_request = $request;
if (is_null($this->last_request))
throw new InvalidOAuth2Request;
if(!$this->last_request->isValid())
if (!$this->last_request->isValid())
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
return $this->token_endpoint->handle($this->last_request);
}
catch(OAuth2BaseException $ex1)
{
} catch (OAuth2BaseException $ex1) {
$this->log_service->warning($ex1);
$this->checkpoint_service->trackException($ex1);
return new OAuth2DirectErrorResponse($ex1->getError(), $ex1->getMessage());
}
catch (Exception $ex)
{
} catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);
@ -1106,7 +1142,8 @@ final class OAuth2Protocol implements IOAuth2Protocol
* @param OAuth2Request $request
* @return OAuth2Response
*/
public function revoke(OAuth2Request $request = null){
public function revoke(OAuth2Request $request = null)
{
try {
$this->last_request = $request;
@ -1114,12 +1151,11 @@ final class OAuth2Protocol implements IOAuth2Protocol
if (is_null($this->last_request))
throw new InvalidOAuth2Request;
if(!$this->last_request->isValid())
if (!$this->last_request->isValid())
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
return $this->revoke_endpoint->handle($this->last_request);
}
catch (Exception $ex) {
} catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);
//simple say "OK" and be on our way ...
@ -1135,31 +1171,24 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
public function introspection(OAuth2Request $request = null)
{
try
{
try {
$this->last_request = $request;
if (is_null($this->last_request))
throw new InvalidOAuth2Request;
if(!$this->last_request->isValid())
if (!$this->last_request->isValid())
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
return $this->introspection_endpoint->handle($this->last_request);
}
catch(ExpiredAccessTokenException $ex1)
{
} catch (ExpiredAccessTokenException $ex1) {
$this->log_service->warning($ex1);
return new OAuth2DirectErrorResponse($ex1->getError(), $ex1->getMessage());
}
catch(OAuth2BaseException $ex2)
{
} catch (OAuth2BaseException $ex2) {
$this->log_service->warning($ex2);
$this->checkpoint_service->trackException($ex2);
return new OAuth2DirectErrorResponse($ex2->getError(), $ex2->getMessage());
}
catch (Exception $ex)
{
} catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);
@ -1183,13 +1212,12 @@ final class OAuth2Protocol implements IOAuth2Protocol
static public function isClientAllowedToUseTokenEndpointAuth(IClient $client)
{
return $client->getClientType() === IClient::ClientType_Confidential ||
$client->getApplicationType() === IClient::ApplicationType_Native;
$client->getApplicationType() === IClient::ApplicationType_Native;
}
static public function getTokenEndpointAuthMethodsPerClientType(IClient $client)
{
if($client->getClientType() == IClient::ClientType_Public)
{
if ($client->getClientType() == IClient::ClientType_Public) {
return ArrayUtils::convert2Assoc
(
array
@ -1219,8 +1247,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
static public function getSigningAlgorithmsPerClientType(IClient $client)
{
if($client->getClientType() == IClient::ClientType_Public)
{
if ($client->getClientType() == IClient::ClientType_Public) {
return ArrayUtils::convert2Assoc
(
array_merge
@ -1254,18 +1281,17 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
static public function getKeyManagementAlgorithmsPerClientType(IClient $client)
{
if($client->getClientType() == IClient::ClientType_Public)
{
if ($client->getClientType() == IClient::ClientType_Public) {
return ArrayUtils::convert2Assoc
(
array_diff
(
self::$supported_key_management_algorithms,
array
(
JSONWebSignatureAndEncryptionAlgorithms::Dir
)
)
array_diff
(
self::$supported_key_management_algorithms,
array
(
JSONWebSignatureAndEncryptionAlgorithms::Dir
)
)
);
}
return ArrayUtils::convert2Assoc
@ -1281,10 +1307,9 @@ final class OAuth2Protocol implements IOAuth2Protocol
public function getJWKSDocument()
{
$keys = $this->server_private_keys_repository->getActives();
$set = [];
$set = [];
foreach($keys as $private_key)
{
foreach ($keys as $private_key) {
$jwk = RSAJWKFactory::build
(
new RSAJWKPEMPrivateKeySpecification
@ -1419,8 +1444,7 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
public function endSession(OAuth2Request $request = null)
{
try
{
try {
$this->log_service->debug_msg("OAuth2Protocol::endSession");
$this->last_request = $request;
@ -1430,59 +1454,59 @@ final class OAuth2Protocol implements IOAuth2Protocol
throw new InvalidOAuth2Request;
}
if(!$this->last_request->isValid()) {
if (!$this->last_request->isValid()) {
$this->log_service->debug_msg(sprintf("OAuth2Protocol::endSession last request is invalid error %s", $this->last_request->getLastValidationError()));
throw new InvalidOAuth2Request($this->last_request->getLastValidationError());
}
if(!$this->last_request instanceof OAuth2LogoutRequest) throw new InvalidOAuth2Request;
if (!$this->last_request instanceof OAuth2LogoutRequest) throw new InvalidOAuth2Request;
$id_token_hint = $this->last_request->getIdTokenHint();
$client_id = null;
$user_id = null;
$user = null;
$client_id = null;
$user_id = null;
$user = null;
if(!empty($id_token_hint)){
if (!empty($id_token_hint)) {
$jwt = BasicJWTFactory::build($id_token_hint);
if((!$jwt instanceof IJWT)) {
if ((!$jwt instanceof IJWT)) {
$this->log_service->debug_msg("OAuth2Protocol::endSession invalid id_token_hint!");
throw new InvalidOAuth2Request('invalid id_token_hint!');
}
$client_id = $jwt->getClaimSet()->getAudience()->getString();
$user_id = $jwt->getClaimSet()->getSubject();
$user_id = $jwt->getClaimSet()->getSubject();
}
if(empty($client_id)){
if (empty($client_id)) {
$client_id = $this->last_request->getClientId();
}
if(is_null($client_id)) {
if (is_null($client_id)) {
$this->log_service->debug_msg("OAuth2Protocol::endSession client_id can not be inferred.");
throw new InvalidClientException('client_id can not be inferred.');
}
$client = $this->client_repository->getClientById($client_id);
if(is_null($client)){
if (is_null($client)) {
$this->log_service->debug_msg("OAuth2Protocol::endSession client not found!");
throw new InvalidClientException('Client not found!');
}
$redirect_logout_uri = $this->last_request->getPostLogoutRedirectUri();
$state = $this->last_request->getState();
$state = $this->last_request->getState();
if(!empty($redirect_logout_uri) && !$client->isPostLogoutUriAllowed($redirect_logout_uri)) {
if (!empty($redirect_logout_uri) && !$client->isPostLogoutUriAllowed($redirect_logout_uri)) {
$this->log_service->debug_msg("OAuth2Protocol::endSession post_logout_redirect_uri not allowed!");
throw new InvalidOAuth2Request('post_logout_redirect_uri not allowed!');
}
if(!is_null($user_id)){
if (!is_null($user_id)) {
// try to get the user from id token ( if its set )
$user_id = $this->auth_service->unwrapUserId(intval($user_id->getString()));
$user = $this->auth_service->getUserById($user_id);
$user = $this->auth_service->getUserById($user_id);
if(is_null($user)){
if (is_null($user)) {
$this->log_service->debug_msg("OAuth2Protocol::endSession user not found!");
throw new InvalidOAuth2Request('user not found!');
}
@ -1490,36 +1514,29 @@ final class OAuth2Protocol implements IOAuth2Protocol
$logged_user = $this->auth_service->getCurrentUser();
if(!is_null($logged_user) && !is_null($user) && $logged_user->getId() !== $user->getId()) {
if (!is_null($logged_user) && !is_null($user) && $logged_user->getId() !== $user->getId()) {
Log::warning(sprintf("OAuth2Protocol::endSession user does not match with current session! logged user id %s - user id %s", $logged_user->getId(), $user->getId()));
}
if(!is_null($logged_user))
if (!is_null($logged_user))
$this->auth_service->logout();
if(!empty($redirect_logout_uri))
{
if (!empty($redirect_logout_uri)) {
return new OAuth2LogoutResponse($redirect_logout_uri, $state);
}
return null;
}
catch (UriNotAllowedException $ex1)
{
} catch (UriNotAllowedException $ex1) {
$this->log_service->warning($ex1);
$this->checkpoint_service->trackException($ex1);
return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient);
}
catch(OAuth2BaseException $ex2)
{
} catch (OAuth2BaseException $ex2) {
$this->log_service->warning($ex2);
$this->checkpoint_service->trackException($ex2);
return new OAuth2DirectErrorResponse($ex2->getError(), $ex2->getMessage());
}
catch (Exception $ex)
{
} catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);

View File

@ -0,0 +1,53 @@
<?php namespace App\libs\OAuth2\Repositories;
/**
* Copyright 2021 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 Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use models\utils\IBaseRepository;
/**
* Interface IOAuth2OTPRepository
* @package App\libs\OAuth2\Repositories
*/
interface IOAuth2OTPRepository extends IBaseRepository
{
/**
* @param string $value
* @return OAuth2OTP|null
*/
public function getByValue(string $value):?OAuth2OTP;
/**
* @param string $connection
* @param string $user_name
* @param Client|null $client
* @return OAuth2OTP|null
*/
public function getByConnectionAndUserNameNotRedeemed
(
string $connection,
string $user_name,
?Client $client
):?OAuth2OTP;
/**
* @param string $user_name
* @param Client|null $client
* @return OAuth2OTP[]
*/
public function getByUserNameNotRedeemed
(
string $user_name,
?Client $client = null
);
}

View File

@ -0,0 +1,117 @@
<?php namespace OAuth2\Requests;
/**
* Copyright 2021 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 OAuth2\OAuth2Protocol;
/**
* Class OAuth2AccessTokenRequestPasswordless
* @package OAuth2\Requests
*/
final class OAuth2AccessTokenRequestPasswordless extends OAuth2TokenRequest
{
public static $params = [
OAuth2Protocol::OAuth2Protocol_GrantType => [
OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless
],
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => [],
OAuth2Protocol::OAuth2PasswordlessConnection => [
OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessConnectionSMS,
] ,
OAuth2Protocol::OAuth2Protocol_Scope => []
];
/**
* @var array
*/
public static $optional_params = [
OAuth2Protocol::OAuth2PasswordlessEmail => [
[
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail
]
],
OAuth2Protocol::OAuth2PasswordlessPhoneNumber => [
[
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionSMS
]
],
];
/**
* Validates current request
* @return bool
*/
public function isValid()
{
$this->last_validation_error = '';
// validate mandatory params
foreach (self::$params as $mandatory_param => $values) {
$mandatory_val = $this->getParam($mandatory_param);
if (empty($mandatory_val)) {
$this->last_validation_error = sprintf("%s not set", $mandatory_param);
return false;
}
if (count($values) > 0 && !in_array($mandatory_val, $values)) {
$this->last_validation_error = sprintf("%s has not a valid value (%s)", $mandatory_param, implode(",", $values));
return false;
}
}
// validate optional params
foreach (self::$optional_params as $optional_param => $rules) {
$optional_param_val = $this->getParam($optional_param);
if (empty($optional_param_val) && count($rules)) continue;
foreach ($rules as $dep_param => $dep_val) {
$dep_param_cur_val = $this->getParam($dep_param);
if ($dep_param_cur_val != $dep_val) continue;
if (empty($optional_param_val)) {
$this->last_validation_error = sprintf("%s not set.", $optional_param);
return false;
}
}
}
return true;
}
public function getConnection(): string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessConnection);
}
public function getEmail(): ?string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessEmail);
}
public function getPhoneNumber(): ?string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessPhoneNumber);
}
public function getUserName(): ?string
{
return $this->getConnection() == OAuth2Protocol::OAuth2PasswordlessConnectionEmail ? $this->getEmail() : $this->getPhoneNumber();
}
public function getScopes():string{
return $this->getParam(OAuth2Protocol::OAuth2Protocol_Scope);
}
public function getOTP():string{
return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType_OTP);
}
}

View File

@ -0,0 +1,127 @@
<?php namespace OAuth2\Requests;
/**
* Copyright 2021 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 OAuth2\OAuth2Protocol;
/**
* Class OAuth2PasswordlessAuthenticationRequest
* @package OAuth2\Requests
*/
final class OAuth2PasswordlessAuthenticationRequest extends OAuth2AuthorizationRequest
{
/**
* @param OAuth2AuthorizationRequest $auth_request
*/
public function __construct(OAuth2AuthorizationRequest $auth_request)
{
parent::__construct($auth_request->getMessage());
}
public static $params = [
OAuth2Protocol::OAuth2Protocol_ResponseType => [
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP
],
OAuth2Protocol::OAuth2Protocol_ClientId => [],
OAuth2Protocol::OAuth2Protocol_Scope => [],
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::ValidOAuth2PasswordlessConnectionValues,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::ValidOAuth2PasswordlessSendValues,
OAuth2Protocol::OAuth2Protocol_Nonce => [],
];
/**
* @var array
*/
public static $optional_params = [
OAuth2Protocol::OAuth2Protocol_RedirectUri => [
[
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendLink
]
],
OAuth2Protocol::OAuth2PasswordlessEmail => [
[
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail
]
],
OAuth2Protocol::OAuth2PasswordlessPhoneNumber => [
[
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionSMS
]
],
];
/**
* Validates current request
* @return bool
*/
public function isValid()
{
$this->last_validation_error = '';
// validate mandatory params
foreach (self::$params as $mandatory_param => $values) {
$mandatory_val = $this->getParam($mandatory_param);
if (empty($mandatory_val)) {
$this->last_validation_error = sprintf("%s not set", $mandatory_param);
return false;
}
if (count($values) > 0 && !in_array($mandatory_val, $values)) {
$this->last_validation_error = sprintf("%s has not a valid value (%s)", $mandatory_param, implode(",", $values));
return false;
}
}
// validate optional params
foreach (self::$optional_params as $optional_param => $rules) {
$optional_param_val = $this->getParam($optional_param);
if (empty($optional_param_val) && count($rules)) continue;
foreach ($rules as $dep_param => $dep_val) {
$dep_param_cur_val = $this->getParam($dep_param);
if ($dep_param_cur_val != $dep_val) continue;
if (empty($optional_param_val)) {
$this->last_validation_error = sprintf("%s not set.", $optional_param);
return false;
}
}
}
return true;
}
public function getConnection(): string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessConnection);
}
public function getSend(): string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessSend);
}
public function getEmail(): ?string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessEmail);
}
public function getNonce(): ?string
{
return $this->getParam(OAuth2Protocol::OAuth2Protocol_Nonce);
}
public function getPhoneNumber(): ?string
{
return $this->getParam(OAuth2Protocol::OAuth2PasswordlessPhoneNumber);
}
}

View File

@ -0,0 +1,39 @@
<?php namespace OAuth2\Responses;
/**
* Copyright 2021 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 OAuth2\OAuth2Protocol;
use Utils\Http\HttpContentType;
/**
* Class OAuth2PasswordlessAuthenticationResponse
* @package OAuth2\Responses
*/
class OAuth2PasswordlessAuthenticationResponse extends OAuth2DirectResponse
{
/**
* OAuth2PasswordlessAuthenticationResponse constructor.
* @param int $otp_length
* @param int $otp_lifetime
* @param string|null $scope
*/
public function __construct(int $otp_length, int $otp_lifetime, ?string $scope = null)
{
// Successful Responses: A server receiving a valid request MUST send a
// response with an HTTP status code of 200.
parent::__construct(self::HttpOkResponse, HttpContentType::Json);
$this["otp_length"] = $otp_length;
$this["otp_lifetime"] = $otp_lifetime;
if(!empty($scope))
$this[OAuth2Protocol::OAuth2Protocol_Scope] = $scope;
}
}

View File

@ -1,19 +0,0 @@
<?php namespace OAuth2\Services;
/**
* Copyright 2015 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.
**/
/**
* Class AccessTokenGenerator
* @package OAuth2\Services
*/
final class AccessTokenGenerator extends OAuth2TokenGenerator {
}

View File

@ -1,20 +0,0 @@
<?php namespace OAuth2\Services;
/**
* Copyright 2015 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.
**/
/**
* Class AuthorizationCodeGenerator
* @package OAuth2\Services
*/
final class AuthorizationCodeGenerator extends OAuth2TokenGenerator {
}

View File

@ -14,16 +14,18 @@
use Auth\User;
use jwt\IBasicJWT;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\Exceptions\InvalidAuthorizationCodeException;
use OAuth2\Exceptions\ReplayAttackException;
use OAuth2\Models\AuthorizationCode;
use OAuth2\Models\AccessToken;
use OAuth2\Models\RefreshToken;
use OAuth2\OAuth2Protocol;
use OAuth2\Exceptions\InvalidAccessTokenException;
use OAuth2\Exceptions\InvalidGrantTypeException;
use OAuth2\Requests\OAuth2AuthorizationRequest;
use Utils\Model\Identifier;
use OAuth2\Requests\OAuth2PasswordlessAuthenticationRequest;
use Utils\Model\AbstractIdentifier;
/**
* Interface ITokenService
@ -37,13 +39,13 @@ interface ITokenService {
* Creates a brand new authorization code
* @param OAuth2AuthorizationRequest $request
* @param bool $has_previous_user_consent
* @return Identifier
* @return AbstractIdentifier
*/
public function createAuthorizationCode
(
OAuth2AuthorizationRequest $request,
bool $has_previous_user_consent = false
):Identifier;
):AbstractIdentifier;
/**
@ -157,7 +159,7 @@ interface ITokenService {
* @param bool $is_hashed
* @return bool
*/
public function clearAccessTokensForRefreshToken($value,$is_hashed = false);
public function clearAccessTokensForRefreshToken($value, $is_hashed = false);
/**
* Mark a given refresh token as void
@ -192,4 +194,31 @@ interface ITokenService {
AccessToken $access_token = null,
AuthorizationCode $auth_code = null
);
/**
* @param OAuth2PasswordlessAuthenticationRequest $request
* @param Client|null $client
* @return OAuth2OTP
* @throws \Exception
*/
public function createOTPFromRequest(OAuth2PasswordlessAuthenticationRequest $request, ?Client $client):OAuth2OTP;
/**
* @param array $payload
* @param Client|null $client
* @return OAuth2OTP
* @throws \Exception
*/
public function createOTPFromPayload(array $payload, ?Client $client):OAuth2OTP;
/**
* @param OAuth2OTP $otp
* @param Client|null $client
* @return AccessToken
*/
public function createAccessTokenFromOTP
(
OAuth2OTP $otp,
?Client $client
):AccessToken;
}

View File

@ -1,19 +0,0 @@
<?php namespace OAuth2\Services;
/**
* Copyright 2015 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.
**/
/**
* Class RefreshTokenGenerator
* @package OAuth2\Services
*/
final class RefreshTokenGenerator extends OAuth2TokenGenerator {
}

View File

@ -42,7 +42,7 @@ final class ClientPKCEAuthContextValidator implements IClientAuthContextValidato
throw new InvalidClientAuthenticationContextException('client not set!');
if ($client->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType())
throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType()));
throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s (%s)', $context->getAuthType(), $client->getTokenEndpointAuthInfo()->getAuthenticationMethod()));
if ($client->getClientType() !== IClient::ClientType_Public)
throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType()));
@ -51,6 +51,6 @@ final class ClientPKCEAuthContextValidator implements IClientAuthContextValidato
Log::debug(sprintf("ClientPKCEAuthContextValidator::validate client id %s - provide client id %s", $client->getClientId(), $providedClientId));
return $client->getClientId() === $providedClientId && $client->isPKCEEnabled();
return $client->getClientId() === $providedClientId && ( $client->isPKCEEnabled() || $client->isPasswordlessEnabled());
}
}

View File

@ -40,7 +40,7 @@ final class ClientPlainCredentialsAuthContextValidator implements IClientAuthCon
throw new InvalidClientAuthenticationContextException('client not set!');
if($client->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType())
throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType()));
throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s (%s)', $context->getAuthType(), $client->getTokenEndpointAuthInfo()->getAuthenticationMethod()));
if($client->getClientType() !== IClient::ClientType_Confidential)
throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType()));

View File

@ -13,12 +13,14 @@
**/
use OpenId\Exceptions\InvalidNonce;
use OpenId\Helpers\OpenIdErrorMessages;
use Utils\Model\Identifier;
use Utils\Model\AbstractIdentifier;
use Zend\Math\Rand;
/**
* Class OpenIdNonce
* @package OpenId\Models
*/
final class OpenIdNonce extends Identifier
final class OpenIdNonce extends AbstractIdentifier
{
const NonceRegexFormat = '/(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z(.*)/';
const NonceTimeFormat = '%Y-%m-%dT%H:%M:%SZ';
@ -139,7 +141,7 @@ final class OpenIdNonce extends Identifier
/**
* @return string
*/
public function getType()
public function getType():string
{
return 'nonce';
}
@ -151,4 +153,27 @@ final class OpenIdNonce extends Identifier
{
return [];
}
/*
* MAY contain additional ASCII characters in the range 33-126 inclusive (printable non-whitespace characters), as necessary to make each response unique
*/
const NoncePopulation = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* Nonce Salt Length
*/
const NonceSaltLength = 32;
/**
* @return string
* @throws InvalidNonce
*/
public function generateValue(): string
{
$salt = Rand::getString(self::NonceSaltLength, self::NoncePopulation);
$date_part = false;
do{ $date_part = gmdate('Y-m-d\TH:i:s\Z'); } while($date_part === false);
$raw_nonce = $date_part. $salt;
$this->setValue($raw_nonce);
return $this->value;
}
}

View File

@ -1,47 +0,0 @@
<?php namespace OpenId\Services;
/**
* 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 Utils\Model\Identifier;
use Utils\Services\UniqueIdentifierGenerator;
use Zend\Math\Rand;
/**
* Class NonceUniqueIdentifierGenerator
* @package OpenId\Services
*/
final class NonceUniqueIdentifierGenerator extends UniqueIdentifierGenerator {
/*
* MAY contain additional ASCII characters in the range 33-126 inclusive (printable non-whitespace characters), as necessary to make each response unique
*/
const NoncePopulation = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* Nonce Salt Length
*/
const NonceSaltLength = 32;
/**
* @param Identifier $identifier
* @return Identifier
*/
protected function _generate(Identifier $identifier){
$salt = Rand::getString(self::NonceSaltLength, self::NoncePopulation, true);
$date_part = false;
do{ $date_part = gmdate('Y-m-d\TH:i:s\Z'); } while($date_part === false);
$raw_nonce = $date_part. $salt;
$identifier->setValue($raw_nonce);
return $identifier;
}
}

View File

@ -0,0 +1,84 @@
<?php namespace Utils\Model;
/**
* Copyright 2015 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 Utils\Services\IdentifierGenerator;
/**
* Class AbstractIdentifier
* @package Utils\Model
*/
abstract class AbstractIdentifier implements Identifier
{
/**
* @param int $len
* @param int $lifetime
*/
public function __construct($len, $lifetime = 0 )
{
$this->lifetime = $lifetime;
$this->len = $len;
}
/**
* @var int
*/
protected $len;
/**
* @var int
*/
protected $lifetime;
/**
* @var string
*/
protected $value;
/**
* @return int
*/
public function getLength():int
{
return $this->len;
}
/**
* @return int
*/
public function getLifetime():int
{
return intval($this->lifetime);
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @param string $value
* @return $this
*/
public function setValue(string $value)
{
$this->value = $value;
return $this;
}
/**
* @return array
*/
abstract public function toArray(): array;
}

View File

@ -1,98 +1,41 @@
<?php namespace Utils\Model;
/**
* Copyright 2015 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 Utils\Services\IdentifierGenerator;
/**
* Class Identifier
* @package Utils\Model
*/
abstract class Identifier
* Copyright 2015 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.
**/
interface Identifier
{
/**
* @param int $len
* @param int $lifetime
*/
public function __construct($len, $lifetime = 0 )
{
$this->lifetime = $lifetime;
$this->len = $len;
}
/**
* @var int
*/
protected $len;
/**
* @var int
*/
protected $lifetime;
/**
* @var string
*/
protected $value;
/**
* @param IdentifierGenerator $generator
* @return $this
*/
public function generate(IdentifierGenerator $generator)
{
return $generator->generate($this);
}
/**
* @return int
*/
public function getLenght()
{
return $this->len;
}
/**
* @return int
*/
public function getLifetime()
{
return intval($this->lifetime);
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
public function getLength():int;
/**
* @param string $value
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
public function setValue(string $value);
/**
* @return int
*/
public function getLifetime():int;
/**
* @return string
*/
abstract public function getType();
public function getType():string;
/**
* @return array
* @return string
*/
abstract public function toArray(): array;
public function generateValue():string;
}

View File

@ -12,7 +12,10 @@
* limitations under the License.
**/
use Auth\Exceptions\AuthenticationException;
use Auth\User;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\Models\IClient;
use OpenId\Models\IOpenIdUser;
/**
@ -33,6 +36,8 @@ interface IAuthService
const AuthenticationResponse_None = "None";
const AuthenticationResponse_Cancel = "Cancel";
const AuthenticationFlowPassword = "password";
const AuthenticationFlowPasswordless = "otp";
/**
* @return bool
*/
@ -47,9 +52,19 @@ interface IAuthService
* @param string $username
* @param string $password
* @param bool $remember_me
* @return mixed
* @return bool
* @throws AuthenticationException
*/
public function login($username, $password, $remember_me);
public function login(string $username, string $password, bool $remember_me): bool;
/**
* @param OAuth2OTP $otpClaim
* @param Client|null $client
* @return OAuth2OTP|null
* @throws AuthenticationException
*/
public function loginWithOTP(OAuth2OTP $otpClaim, ?Client $client = null): ?OAuth2OTP;
/**
* @param string $username

View File

@ -22,5 +22,5 @@ interface IdentifierGenerator {
* @param Identifier $identifier
* @return Identifier
*/
public function generate(Identifier $identifier);
public function generate(Identifier $identifier):Identifier;
}

View File

@ -17,7 +17,7 @@ use Zend\Crypt\Hash;
* Class UniqueIdentifierGenerator
* @package Utils\Services
*/
abstract class UniqueIdentifierGenerator implements IdentifierGenerator
class UniqueIdentifierGenerator implements IdentifierGenerator
{
/**
@ -37,20 +37,13 @@ abstract class UniqueIdentifierGenerator implements IdentifierGenerator
* @param Identifier $identifier
* @return Identifier
*/
public function generate(Identifier $identifier){
public function generate(Identifier $identifier):Identifier{
do
{
$key = sprintf("%s.%s", $identifier->getType(), Hash::compute('sha256', $this->_generate($identifier)->getValue()));
$key = sprintf("%s.%s", $identifier->getType(), Hash::compute('sha256', $identifier->generateValue()));
}
while(!$this->cache_service->addSingleValue($key, $key));
return $identifier;
}
/**
* @param Identifier $identifier
* @return Identifier
*/
abstract protected function _generate(Identifier $identifier);
}

7
config/otp.php Normal file
View File

@ -0,0 +1,7 @@
<?php
return [
"lifetime" => env("OTP_DEFAULT_LIFETIME", 120),
"length" => env("OTP_DEFAULT_LENGTH", 6)
];

View File

@ -0,0 +1,93 @@
<?php namespace Database\Migrations;
/**
* Copyright 2021 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 Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use LaravelDoctrine\Migrations\Schema\Builder;
use LaravelDoctrine\Migrations\Schema\Table;
/**
* Class Version20210616123839
* @package Database\Migrations
*/
class Version20210616123839 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema): void
{
$builder = new Builder($schema);
if (!$builder->hasTable("oauth2_otp")) {
$builder->create("oauth2_otp", function (Table $table) {
$table->bigInteger("id", true, false);
$table->primary("id");
$table->timestamps();
$table->string("value")->setLength(50)->setNotnull(true);
$table->string("connection")->setLength(10)->setNotnull(true);//sms|mail
$table->string("send")->setLength(10)->setNotnull(true);//code|link
$table->text("scope")->setNotnull(false);
$table->string("email")->setLength(50)->setNotnull(false);
$table->string("phone_number")->setLength(50)->setNotnull(false);
$table->string("nonce")->setLength(50)->setNotnull(false);
$table->integer("redeemed_attempts")->setNotnull(true)->setDefault(0);
$table->string("redeemed_from_ip")->setNotnull(false);
$table->string("redirect_url")->setLength(255)->setNotnull(false);
$table->integer('length')->setNotnull(true)->setDefault(6);
// seconds
$table->integer('lifetime')->setNotnull(true)->setDefault(60);
$table->dateTime('redeemed_at')->setNotnull(false);
// FK Optional
$table->unsignedBigInteger("oauth2_client_id", false)->setNotnull(false)->setDefault('NULL');
$table->index("oauth2_client_id", "oauth2_client_id");
$table->foreign("oauth2_client", "oauth2_client_id", "id", ["onDelete" => "CASCADE"]);
$table->unique(["oauth2_client_id", "value"]);
});
}
if ($builder->hasTable("oauth2_client") && !$builder->hasColumn("oauth2_client","otp_enabled")) {
$builder->table('oauth2_client', function (Table $table) {
//
$table->boolean('otp_enabled')->setNotnull(false)->setDefault(0);
// characters
$table->integer('otp_length')->setNotnull(false)->setDefault(6);
// seconds
$table->integer('otp_lifetime')->setNotnull(false);
});
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema): void
{
$builder = new Builder($schema);
if ($builder->hasTable("oauth2_otp")) {
$builder->drop("oauth2_otp");
}
if ($builder->hasTable("oauth2_client") && $builder->hasColumn("oauth2_client","otp_enabled")) {
$builder->table('oauth2_client', function (Table $table) {
//
$table->dropColumn('otp_enabled');
// characters
$table->dropColumn('otp_length');
// seconds
$table->dropColumn('otp_lifetime');
});
}
}
}

View File

@ -0,0 +1,53 @@
<?php namespace Database\Migrations;
/**
* Copyright 2021 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 Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
/**
* Class Version20210616123841
* @package Database\Migrations
*/
class Version20210616123841 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema): void
{
$sql = <<<SQL
ALTER TABLE oauth2_otp MODIFY `connection`
enum(
'sms',
'email'
) default 'email' null;
SQL;
$this->addSql($sql);
$sql = <<<SQL
ALTER TABLE oauth2_otp MODIFY send
enum(
'code',
'link'
) default 'code' null;
SQL;
$this->addSql($sql);
}
/**
* @param Schema $schema
*/
public function down(Schema $schema): void
{
}
}

View File

@ -24,6 +24,31 @@ jQuery(document).ready(function($){
}
});
if($("#otp_enabled") .is(":checked")){
$(".otp_controls").removeClass("hidden");
$("#otp_length").rules("add", {required:true, min:4, max:8});
$("#otp_lifetime").rules("add", {required:true, min:60, max:600});
}
else {
$(".otp_controls").addClass("hidden");
$("#otp_length").rules("remove");
$("#otp_lifetime").rules("remove");
}
$("#otp_enabled").change(function() {
if(this.checked) {
$(".otp_controls").removeClass("hidden");
$("#otp_length").rules("add", {required:true, min:4, max:8});
$("#otp_lifetime").rules("add", {required:true, min:60, max:600});
return true;
}
$(".otp_controls").addClass("hidden");
$("#otp_length").rules("remove");
$("#otp_lifetime").rules("remove");
return true;
});
$('#token_endpoint_auth_method').change(function() {
var auth_method = $(this).val();

View File

@ -33,6 +33,10 @@ chmod 777 vendor/ezyang/htmlpurifier/library/HTMLPurifier/DefinitionCache/Serial
Laravel may require some permissions to be configured: folders within storage and vendor require write access by the web server.
## validate schema
php artisan doctrine:schema:validate
## create schema
php artisan doctrine:schema:create --sql --em=model > model.sql

View File

@ -20,7 +20,7 @@ const xhrs = {};
const cancel = (key) => {
if(xhrs[key]) {
xhrs[key].abort();
xhrs[key].xhr.abort();
console.log(`aborted request ${key}`);
delete xhrs[key];
}
@ -31,11 +31,15 @@ const schedule = (key, req) => {
xhrs[key] = req;
};
const end = (key) => {
delete xhrs[key];
}
const isObjectEmpty = (obj) => {
return Object.keys(obj).length === 0 && obj.constructor === Object ;
}
export const getRawRequest = (endpoint, errorHandler = null) => (params) => {
export const getRawRequest = (endpoint) => (params) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
@ -45,26 +49,47 @@ export const getRawRequest = (endpoint, errorHandler = null) => (params) => {
cancel(key);
return new Promise((resolve, reject) => {
let req = http.get(url.toString())
.timeout({
response: 60000,
deadline: 60000,
})
.end(
(err, res) => {
if (err || !res.ok) {
if(errorHandler) {
errorHandler(err, res);
}
return reject({ err, res })
}
let json = res.body;
return resolve({response: json});
}
)
let req = http.get(url.toString());
schedule(key, req);
return req.timeout({
response: 60000,
deadline: 60000,
}).then((res) => {
let json = res.body;
end(key);
return Promise.resolve({response: json});
}).catch((error) => {
end(key);
return Promise.reject(error);
})
}
export const postRawRequest = (endpoint) => (params, headers = {}) => {
let url = URI(endpoint);
if(!isObjectEmpty(params))
url = url.query(params);
let key = url.toString();
cancel(key);
let req = http.post(url.toString());
schedule(key, req);
return req.set(headers).send(params).timeout({
response: 60000,
deadline: 60000,
}).then((res) => {
let json = res.body;
end(key);
return Promise.resolve({response: json});
}).catch((error) => {
end(key);
return Promise.reject(error);
})
schedule(key, req);
});
}

View File

@ -1,8 +1,8 @@
import {getRawRequest} from '../base_actions'
import {getRawRequest, postRawRequest} from '../base_actions'
export const verifyAccount = (email) => {
const params = {
email: email
};
@ -10,3 +10,13 @@ export const verifyAccount = (email) => {
return getRawRequest(window.VERIFY_ACCOUNT_ENDPOINT)(params);
}
export const emitOTP = (email, token, connection = 'email', send='code') => {
const params = {
username: email,
connection:connection,
send:send
}
return postRawRequest(window.EMIT_OTP_ENDPOINT)(params, {'X-CSRF-TOKEN': token});
}

View File

@ -14,7 +14,7 @@ import Container from '@material-ui/core/Container';
import Chip from '@material-ui/core/Chip';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import {verifyAccount} from './actions';
import {verifyAccount, emitOTP} from './actions';
import {MuiThemeProvider, createMuiTheme} from '@material-ui/core/styles';
import DividerWithText from '../components/divider_with_text';
import Visibility from '@material-ui/icons/Visibility';
@ -23,7 +23,7 @@ import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import {emailValidator} from '../validator';
import Grid from '@material-ui/core/Grid';
import Swal from 'sweetalert2'
const EmailInputForm = ({onValidateEmail, onHandleUserNameChange, disableInput, emailError}) => {
@ -117,6 +117,7 @@ const PasswordInputForm = ({
/>
<input type="hidden" value={userNameValue} id="username" name="username"/>
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
<input type="hidden" value="password" id="flow" name="flow"/>
{shouldShowCaptcha() &&
<ReCAPTCHA
className={styles.recaptcha}
@ -136,10 +137,91 @@ const PasswordInputForm = ({
);
}
const HelpLinks = ({forgotPasswordAction, verifyEmailAction, helpAction, appName}) => {
const OTPInputForm = ({
formAction,
onAuthenticate,
disableInput,
showPassword,
passwordValue,
passwordError,
onUserPasswordChange,
handleClickShowPassword,
handleMouseDownPassword,
userNameValue,
csrfToken,
shouldShowCaptcha,
captchaPublicKey,
onChangeRecaptcha
}) => {
return(
<form method="post" action={formAction} onSubmit={onAuthenticate}>
<TextField
id="password"
name="password"
disabled={disableInput}
type={showPassword ? 'text' : 'password'}
value={passwordValue}
variant="outlined"
margin="normal"
required
fullWidth
label="Enter Your verification code"
autoComplete="new-password"
error={passwordError != ""}
helperText={passwordError}
onChange={onUserPasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle code visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <Visibility/> : <VisibilityOff/>}
</IconButton>
</InputAdornment>
)
}}
/>
<p>A Verification Code was just sent to your Email.</p>
<FormControlLabel
disabled={disableInput}
control={<Checkbox value="remember" name="remember" id="remember" color="primary"/>}
label="Remember me"
/>
<input type="hidden" value={userNameValue} id="username" name="username"/>
<input type="hidden" value={csrfToken} id="_token" name="_token"/>
<input type="hidden" value="otp" id="flow" name="flow"/>
<input type="hidden" value="email" id="connection" name="connection"/>
{shouldShowCaptcha() &&
<ReCAPTCHA
className={styles.recaptcha}
sitekey={captchaPublicKey}
onChange={onChangeRecaptcha}
/>
}
<Button variant="contained"
disabled={disableInput}
className={styles.continue_btn}
color="primary"
type="submit"
onClick={onAuthenticate}>
Verify
</Button>
</form>
);
}
const HelpLinks = ({forgotPasswordAction, verifyEmailAction, helpAction, appName, emitOtpAction}) => {
return (
<>
<hr className={styles.separator}/>
<Link href="#" onClick={emitOtpAction} variant="body2" target="_self" >
Get A Login Code emailed to you
</Link>
<Link href={forgotPasswordAction} target="_self" variant="body2">
Forgot password?
</Link>
@ -153,6 +235,17 @@ const HelpLinks = ({forgotPasswordAction, verifyEmailAction, helpAction, appName
);
}
const OTPHelpLinks = ({emitOtpAction}) => {
return (
<>
<hr className={styles.separator}/>
<p className={styles.otp_p}>Didn't receive it ?</p>
<p className={styles.otp_p}>Check your spam folder or <Link href="#" onClick={emitOtpAction} variant="body2" target="_self">resend email.</Link>
</p>
</>
);
}
const EmailErrorActions = ({createAccountAction, onValidateEmail, disableInput}) => {
return(
<Grid container style={{alignItems: 'center', marginTop: "20%"}}>
@ -201,6 +294,7 @@ class LoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
user_name: props.userName,
user_password: '',
@ -214,7 +308,9 @@ class LoginPage extends React.Component {
captcha_value: '',
showPassword: false,
disableInput: false,
authFlow: props.flow,
}
this.onHandleUserNameChange = this.onHandleUserNameChange.bind(this);
this.onValidateEmail = this.onValidateEmail.bind(this);
this.handleDelete = this.handleDelete.bind(this);
@ -224,6 +320,29 @@ class LoginPage extends React.Component {
this.shouldShowCaptcha = this.shouldShowCaptcha.bind(this);
this.handleClickShowPassword = this.handleClickShowPassword.bind(this);
this.handleMouseDownPassword = this.handleMouseDownPassword.bind(this);
this.handleEmitOtpAction = this.handleEmitOtpAction.bind(this);
}
handleEmitOtpAction(ev){
ev.preventDefault();
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;
this.setState({...this.state,
authFlow:"otp",
errors: {
email: "",
password:"",
},
user_verified: true,
user_fullname:user_fullname,
});
}, (error) => {
let {response, status, message} = error;
Swal('Oops...', 'Something went wrong!', 'error')
});
return false;
}
shouldShowCaptcha() {
@ -236,7 +355,11 @@ class LoginPage extends React.Component {
onAuthenticate(ev) {
if (this.state.user_password == '') {
this.setState({...this.state, errors: {...this.state.errors, password: 'Password is empty'}});
let error = 'Password is empty';
if(this.state.authFlow == 'OTP'){
error = 'Verification Code is empty';
}
this.setState({...this.state, errors: {...this.state.errors, password: error}});
ev.preventDefault();
return false;
}
@ -276,9 +399,11 @@ class LoginPage extends React.Component {
if (!emailValidator(this.state.user_name)) {
return false;
}
this.setState({...this.state, disableInput: true});
verifyAccount(this.state.user_name).then((payload) => {
let {response} = payload;
this.setState({
...this.state,
user_pic: response.pic,
@ -291,10 +416,18 @@ class LoginPage extends React.Component {
disableInput: false
})
}, (error) => {
let {body} = error.res;
let newErrors = {}
let {response, status, message} = error;
let newErrors = {};
newErrors['password'] = '';
newErrors['email'] = "We could not find an Account with that email Address";
if(status == 429){
newErrors['email'] = "Too many requests. Try it later.";
}
this.setState({
...this.state,
user_pic: null,
@ -308,7 +441,10 @@ class LoginPage extends React.Component {
}
handleDelete() {
this.setState({...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false});
this.setState({...this.state, user_name: null, user_pic: null, user_fullname: null, user_verified: false, authFlow:"password", errors: {
email: "",
password:"",
},});
}
handleClickShowPassword(ev) {
@ -365,12 +501,13 @@ class LoginPage extends React.Component {
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
emitOtpAction={this.handleEmitOtpAction}
/>
</>
}
</>
}
{this.state.user_verified &&
{this.state.user_verified && this.state.authFlow == 'password' &&
// proceed to ask for password ( 2nd step )
<>
<PasswordInputForm
@ -394,9 +531,32 @@ class LoginPage extends React.Component {
forgotPasswordAction={this.props.forgotPasswordAction}
verifyEmailAction={this.props.verifyEmailAction}
helpAction={this.props.helpAction}
emitOtpAction={this.handleEmitOtpAction}
/>
</>
}
{this.state.user_verified && this.state.authFlow == 'otp' &&
// proceed to ask for password ( 2nd step )
<>
<OTPInputForm
formAction={this.props.formAction}
onAuthenticate={this.onAuthenticate}
disableInput={this.state.disableInput}
showPassword={this.state.showPassword}
passwordValue={this.state.user_password}
passwordError={this.state.errors.password}
onUserPasswordChange={this.onUserPasswordChange}
handleClickShowPassword={this.handleClickShowPassword}
handleMouseDownPassword={this.handleMouseDownPassword}
userNameValue={this.state.user_name}
csrfToken={this.props.token}
shouldShowCaptcha={this.shouldShowCaptcha}
captchaPublicKey={this.props.captchaPublicKey}
onChangeRecaptcha={this.onChangeRecaptcha}
/>
<OTPHelpLinks emitOtpAction={this.handleEmitOtpAction}/>
</>
}
</div>
</Container>
);

View File

@ -8,6 +8,7 @@ $base-color:#3fa2f7;
color: $base-color;
}
.inner_container {
display: flex;
margin-top: 64px;
@ -71,4 +72,9 @@ $base-color:#3fa2f7;
.valid_user_name_chip{
float: right;
}
.otp_p {
margin: 0;
padding: 0;
}

View File

@ -32,8 +32,10 @@
appLogo: '{{ Config::get("app.logo_url") }}',
formAction: '{{ URL::action("UserController@postLogin") }}',
accountVerifyAction : '{{URL::action("UserController@getAccount")}}',
emitOtpAction : '{{URL::action("UserController@emitOTP")}}',
authError: authError,
captchaPublicKey: '{{ Config::get("recaptcha.public_key") }}',
flow: 'password',
thirdPartyProviders: [
@foreach($supported_providers as $provider => $label)
{label: "{{$label}}", name:"{{$provider}}"},
@ -63,9 +65,12 @@
@if(Session::has('user_verified'))
config.user_verified = {{Session::get('user_verified')}};
@endif
@if(Session::has('flow'))
config.flow = '{{Session::get('flow')}}';
@endif
window.VERIFY_ACCOUNT_ENDPOINT = config.accountVerifyAction;
window.EMIT_OTP_ENDPOINT = config.emitOtpAction;
</script>
{!! HTML::script('assets/login.js') !!}
@append

View File

@ -0,0 +1,202 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<!--
** try it at https://mjml.io/try-it-live/
<mjml>
<mj-body background-color="#fafbfc">
<mj-section padding-bottom="20px" padding-top="20px">
<mj-column vertical-align="middle" width="100%">
<mj-image align="center" padding="25px" src="{{Config::get('app.logo_url')}}" width="125px"></mj-image>
</mj-column>
</mj-section>
<mj-section background-color="#fff" padding-bottom="20px" padding-top="20px">
<mj-column vertical-align="middle" width="100%">
<mj-text align="center" font-size="16px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px"><span>Hello,</span></mj-text>
<mj-text align="center" font-size="16px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px">Please use the verification code below on the {{Config::get('app.app_name')}} website:</mj-text>
<mj-text align="center" font-size="24px" background-color="#20c997" font-weight="bold" font-family="open Sans Helvetica, Arial, sans-serif">{{$otp}}</mj-text>
<mj-text align="center" font-size="16px" font-family="open Sans Helvetica, Arial, sans-serif"> Should be valid for {{$lifetime}} minutes.</mj-text>
<mj-text align="center" font-size="16px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="16px">If you didn't request this, you can ignore this email or let us know.</mj-text>
<mj-text align="center" font-size="16px" font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px">Thanks! <br />{{Config::get('app.tenant_name')}} Support Team</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
-->
<head>
<title>
{{$subject}}
</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing:normal;background-color:#fafbfc;">
<div style="background-color:#fafbfc;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:20px;padding-top:20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:125px;">
<img height="auto" src="{{Config::get('app.logo_url')}}" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="125" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;padding-bottom:20px;padding-top:20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:middle;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:middle;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:center;color:#000000;"><span>Hello,</span></div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:center;color:#000000;">Please use the verification code below on the {{Config::get('app.app_name')}} website:</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:24px;font-weight:bold;line-height:1;text-align:center;color:#000000;">{{$otp}}</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:center;color:#000000;">Should be valid for {{$lifetime}} minutes.</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:16px;padding-left:25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:center;color:#000000;">If you didn't request this, you can ignore this email or let us know.</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<div style="font-family:open Sans Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:center;color:#000000;">Thanks! <br />{{Config::get('app.tenant_name')}} Support Team</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -9,23 +9,23 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#"></a>
<a class="navbar-brand" href="#" target="_self"></a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul id='main-menu' class="nav navbar-nav">
<li id="profile"><a href='{!! URL::action("UserController@getProfile") !!}'>{{ __('Settings') }}</a></li>
<li id="profile"><a target="_self" href='{!! URL::action("UserController@getProfile") !!}'>{{ __('Settings') }}</a></li>
<li id="oauth2-console" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<a target="_self" href="#" class="dropdown-toggle" data-toggle="dropdown">
{{ __('OAUTH2 Console') }}<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href='{!!URL::action("AdminController@listOAuth2Clients")!!}'>{{ __('OAUTH2 Applications') }}</a></li>
<li><a href='{!!URL::action("AdminController@editIssuedGrants")!!}'>{{ __('Issued OAUTH2 Grants') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listOAuth2Clients")!!}'>{{ __('OAUTH2 Applications') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@editIssuedGrants")!!}'>{{ __('Issued OAUTH2 Grants') }}</a></li>
</ul>
</li>
@if(Auth::user()->isOpenIdServerAdmin() || Auth::user()->isOAuth2ServerAdmin() || Auth::user()->isSuperAdmin())
<li id='server-admin' class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<a target="_self" href="#" class="dropdown-toggle" data-toggle="dropdown">
{{ __('Server Administration') }}
<b class="caret"></b>
</a>
@ -33,29 +33,29 @@
@if(Auth::user()->isSuperAdmin() || Auth::user()->isOpenIdServerAdmin())
<li class="dropdown-header">{{ __('Security') }}</li>
@if(Auth::user()->isSuperAdmin())
<li><a href='{!!URL::action("AdminController@listUsers")!!}'>{{ __('Users') }}</a></li>
<li><a href='{!!URL::action("AdminController@listGroups")!!}'>{{ __('Groups') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listUsers")!!}'>{{ __('Users') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listGroups")!!}'>{{ __('Groups') }}</a></li>
@endif
<li><a href='{!!URL::action("AdminController@listBannedIPs")!!}'>{{ __('Banned IPs') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listBannedIPs")!!}'>{{ __('Banned IPs') }}</a></li>
<li role="separator" class="divider"></li>
@endif
@if(Auth::user()->isOAuth2ServerAdmin())
<li class="dropdown-header">{{ __('OAUTH2') }}</li>
<li><a href='{!!URL::action("AdminController@listServerPrivateKeys")!!}'>{{ __('Private Keys') }}</a></li>
<li><a href='{!!URL::action("AdminController@listResourceServers")!!}'>{{ __('Resource Servers') }}</a></li>
<li><a href='{!!URL::action("AdminController@listApiScopeGroups")!!}'>{{ __('Api Scope Groups') }}</a></li>
<li><a href='{!!URL::action("AdminController@listLockedClients")!!}'>{{ __('Locked Clients') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listServerPrivateKeys")!!}'>{{ __('Private Keys') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listResourceServers")!!}'>{{ __('Resource Servers') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listApiScopeGroups")!!}'>{{ __('Api Scope Groups') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listLockedClients")!!}'>{{ __('Locked Clients') }}</a></li>
<li role="separator" class="divider"></li>
@endif
@if(Auth::user()->isOpenIdServerAdmin())
<li class="dropdown-header">{{ __('Server') }}</li>
<li><a href='{!!URL::action("AdminController@listServerConfig")!!}'>{{ __('Server Configuration') }}</a></li>
<li><a target="_self" href='{!!URL::action("AdminController@listServerConfig")!!}'>{{ __('Server Configuration') }}</a></li>
@endif
</ul>
</li>
@endif
<li><a title="help" target="_blank" href="mailto:{!! Config::get("app.help_email") !!}">Help</a></li>
<li><a href='{!! URL::action("UserController@logout") !!}'>Logout</a></li>
<li><a target="_self" href='{!! URL::action("UserController@logout") !!}'>Logout</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>

View File

@ -10,11 +10,41 @@
id="pkce_enabled">
Use PCKE?
&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true" title="Use Proof Key for Code Exchange instead of a Client Secret ( Public Clients)"></span>
aria-hidden="true" title="Use Proof Key for Code Exchange instead of a Client Secret (Public Clients)"></span>
</label>
</div>
</div>
@endif
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox"
@if ($client->otp_enabled)
checked
@endif
id="otp_enabled">
Use Passwordless?
&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true" title="Use Passwordless Authentication"></span>
</label>
</div>
</div>
<div class="form-group hidden otp_controls">
<label for="otp_length">OTP Length
<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true"
title="One Time Password Length"></span></label>
<input type="number" name="otp_length" class="form-control" id="otp_length"
value="{!!$client->otp_length!!}">
</div>
<div class="form-group hidden otp_controls">
<label for="otp_lifetime">OTP LifeTime (Seconds)
<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true"
title="One Time Password span lifetime in seconds"></span></label>
<input type="number" name="otp_lifetime" class="form-control" id="otp_lifetime"
value="{!!$client->otp_lifetime!!}">
</div>
<div class="form-group">
<label for="default_max_age">Default Max. Age (optional)&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true"

View File

@ -44,7 +44,8 @@ Route::group(array('middleware' => ['ssl']), function () {
Route::group(array('prefix' => 'login'), function () {
Route::get('', "UserController@getLogin");
Route::get('account-verify', "UserController@getAccount");
Route::get('account-verify', ['middleware' => ['csrf', 'throttle:account'], 'uses' => 'UserController@getAccount']);
Route::post('otp', ['middleware' => ['csrf', 'throttle:otp'], 'uses' => 'UserController@emitOTP']);
Route::post('', ['middleware' => 'csrf', 'uses' => 'UserController@postLogin']);
Route::get('cancel', "UserController@cancelLogin");
Route::group(array('prefix' => '{provider}'), function () {
@ -56,7 +57,7 @@ Route::group(array('middleware' => ['ssl']), function () {
// registration routes
Route::group(array('prefix' => 'register'), function () {
Route::get('', 'Auth\RegisterController@showRegistrationForm');
Route::post('', ['middleware' => 'csrf', 'uses' => 'Auth\RegisterController@register']);
Route::post('', ['middleware' => ['csrf'], 'uses' => 'Auth\RegisterController@register']);
});
Route::group(array('prefix' => 'verification'), function () {
@ -100,7 +101,8 @@ Route::group(['namespace' => 'OAuth2', 'middleware' => ['ssl']], function () {
Route::get('/.well-known/openid-configuration', "OAuth2ProviderController@discovery");
});
Route::group(['namespace' => 'OAuth2', 'prefix' => 'oauth2', 'middleware' => ['ssl']], function () {
Route::group(['namespace' => 'OAuth2', 'prefix' => 'oauth2', 'middleware' => ['ssl','throttle:oauth2']], function () {
Route::get('/check-session', "OAuth2ProviderController@checkSessionIFrame");
Route::get('/end-session', "OAuth2ProviderController@endSession");
Route::post('/end-session', "OAuth2ProviderController@endSession");

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,8 +14,8 @@
use Models\OAuth2\ApiEndpoint;
use Models\OAuth2\Api;
use Models\OAuth2\ApiScope;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\Config;
/**
* Class ApiEndpointTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,8 +13,8 @@
**/
use Models\OAuth2\ApiScope;
use Models\OAuth2\Api;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\Config;
/**
* Class ApiScopeTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,8 +13,8 @@
**/
use Models\OAuth2\Api;
use Models\OAuth2\ResourceServer;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\Config;
/**
* Class ApiTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -16,7 +16,7 @@ use OpenId\Helpers\AssociationFactory;
use OpenId\OpenIdProtocol;
use Utils\Services\UtilsServiceCatalog;
use Utils\Exceptions\UnacquiredLockException;
use Tests\BrowserKitTestCase;
use Mockery;
/**
* Class AssociationServiceTest
*/

View File

@ -11,8 +11,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -20,7 +20,7 @@ use LaravelDoctrine\ORM\Facades\EntityManager;
/**
* Class ClientApiTest
*/
class ClientApiTest extends \Tests\BrowserKitTestCase {
class ClientApiTest extends BrowserKitTestCase {
private $current_realm;

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -16,8 +16,9 @@ use jwk\JSONWebKeyPublicKeyUseValues;
use Models\OAuth2\Client;
use jwa\JSONWebSignatureAndEncryptionAlgorithms;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Tests\BrowserKitTestCase;
use Auth\User;
use Illuminate\Support\Facades\Config;
use TestKeys;
/**
* Class ClientPublicKeyApiTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -16,7 +16,6 @@ use Utils\Services\UtilsServiceCatalog;
use OpenId\Services\OpenIdServiceCatalog;
use Auth\Repositories\IUserRepository;
use Auth\IAuthenticationExtensionService;
use Tests\TestCase;
use Illuminate\Support\Facades\App;
/**
* Class CustomAuthProviderTest

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -15,7 +15,6 @@ use OpenId\Helpers\AssocHandleGenerator;
use OpenId\Helpers\OpenIdCryptoHelper;
use OpenId\Requests\OpenIdDHAssociationSessionRequest;
use Zend\Crypt\PublicKey\DiffieHellman;
use Tests\TestCase;
/**
* Class DiffieHellmanTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -11,7 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Tests\BrowserKitTestCase;
/***
* Class DiscoveryControllerTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -17,7 +17,6 @@ use Utils\Services\IAuthService;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Config;
use LaravelDoctrine\ORM\Facades\EntityManager;
/**
* Class OAuth2ProtectedApiTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -12,13 +12,13 @@
* limitations under the License.
**/
use Auth\User;
use Illuminate\Support\Facades\App;
use OAuth2\OAuth2Protocol;
use Utils\Services\IAuthService;
use Utils\Services\UtilsServiceCatalog;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Config;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\DB;
/**
* Class OAuth2ProtocolTest
* Test Suite for OAuth2 Protocol

View File

@ -11,7 +11,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2ProtectedApiTest;
use App\libs\OAuth2\IUserScopes;
/**
* Class OAuth2UserRegistrationServiceApiTest

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,7 @@
**/
use LaravelDoctrine\ORM\Facades\EntityManager;
use LaravelDoctrine\ORM\Facades\Registry;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\Persistence\ObjectRepository;
use Illuminate\Support\Facades\DB;
use App\Models\SSO\DisqusSSOProfile;
use App\Models\Utils\BaseEntity;

View File

@ -0,0 +1,557 @@
<?php namespace Tests;
/**
* Copyright 2021 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\Mail\OAuth2PasswordlessOTPMail;
use Illuminate\Support\Facades\Mail;
use jwe\IJWE;
use jwk\impl\RSAJWKFactory;
use jwk\JSONWebKeyPublicKeyUseValues;
use jws\IJWS;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\OAuth2Protocol;
use utils\factories\BasicJWTFactory;
use Utils\Services\UtilsServiceCatalog;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Session;
/**
* Class OIDCPasswordlessTest
* @package Tests
*/
class OIDCPasswordlessTest extends OpenStackIDBaseTest
{
/**
* @var Client
*/
public static $client = null;
protected function setUp():void
{
parent::setUp();
$client_repository = EntityManager::getRepository(Client::class);
$clients = $client_repository->findAll();
self::$client = $clients[0];
self::$client->enablePasswordless();
self::$client->setOtpLifetime(60 * 3);
self::$client->setOtpLength(6);
self::$client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic);
EntityManager::persist(self::$client);
}
protected function tearDown():void
{
parent::tearDown();
}
/**
* @var string
*/
private $current_realm;
protected function prepareForTests()
{
parent::prepareForTests();
App::singleton(UtilsServiceCatalog::ServerConfigurationService, StubServerConfigurationService::class);
$this->current_realm = Config::get('app.url');
Session::start();
}
public function testCodeEmailFlowErrorScopes(){
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// ask for wider scopes
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
$content = $response->getContent();
$response = json_decode($content);
$this->assertTrue(!empty($response->error));
}
public function testCodeEmailFlowError(){
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendLink,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(400);
}
public function testCodeEmailFlowNoRefreshToken(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
$access_token = $response->access_token;
$id_token = $response->id_token;
$this->assertTrue(!empty($access_token));
$this->assertTrue(!property_exists($response, "refresh_token"));
$this->assertTrue(!empty($id_token));
}
public function testCodeEmailFlowConsecutiveOTP(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp1 = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp1){
$otp1 = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp2 = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp2){
$otp2 = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
$repository = EntityManager::getRepository(OAuth2OTP::class);
$otp1 = $repository->getByValue($otp1);
$this->assertTrue(is_null($otp1));
$otp2 = $repository->getByValue($otp2);
$this->assertTrue(!is_null($otp2));
}
public function testCodeEmailFlowNarrowScopes(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// ask for wider scopes
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$content = $response->getContent();
$response = json_decode($content);
$this->assertTrue(!empty($response->id_token));
}
public function testCodeEmailFlow() {
$scope = sprintf('%s profile email address %s',
OAuth2Protocol::OpenIdConnect_Scope,
OAuth2Protocol::OfflineAccess_Scope
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
$access_token = $response->access_token;
$refresh_token = $response->refresh_token;
$id_token = $response->id_token;
$this->assertTrue(!empty($access_token));
$this->assertTrue(!empty($refresh_token));
$this->assertTrue(!empty($id_token));
$jwt = BasicJWTFactory::build($id_token);
$use_enc = false;
if ($use_enc) {
$this->assertTrue($jwt instanceof IJWE);
$recipient_key = RSAJWKFactory::build
(
new RSAJWKPEMPrivateKeySpecification
(
TestSeeder::$client_private_key_1,
RSAJWKPEMPrivateKeySpecification::WithoutPassword,
$jwt->getJOSEHeader()->getAlgorithm()->getString()
)
);
$recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key');
$jwt->setRecipientKey($recipient_key);
$payload = $jwt->getPlainText();
$jwt = BasicJWTFactory::build($payload);
$this->assertTrue($jwt instanceof IJWS);
}
return $access_token;
}
public function testInvalidRedeemCodeEmailFlow() {
$scope = sprintf('%s profile email address %s',
OAuth2Protocol::OpenIdConnect_Scope,
OAuth2Protocol::OfflineAccess_Scope
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'1',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'2',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'3',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
$repository = EntityManager::getRepository(OAuth2OTP::class);
$otp = $repository->getByValue($otp);
$this->assertTrue(!is_null($otp));
$this->assertTrue(!$otp->isValid());
}
}

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Auth\User;
use Illuminate\Support\Facades\App;
use jwa\JSONWebSignatureAndEncryptionAlgorithms;
@ -35,6 +36,9 @@ use jwt\impl\UnsecuredJWT;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\Session;
use Database\Seeders\TestSeeder;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
/**
* Class OIDCProtocolTest
* http://openid.net/wordpress-content/uploads/2015/02/OpenID-Connect-Conformance-Profiles.pdf
@ -49,7 +53,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
protected function prepareForTests()
{
parent::prepareForTests();
App::singleton(UtilsServiceCatalog::ServerConfigurationService, 'StubServerConfigurationService');
App::singleton(UtilsServiceCatalog::ServerConfigurationService, StubServerConfigurationService::class);
$this->current_realm = Config::get('app.url');
Session::start();
}
@ -99,9 +103,9 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
'response_type' => 'code',
'scope' => 'openid profile email',
OAuth2Protocol::OAuth2Protocol_LoginHint => ' sebastian@tipit.net ',
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200,
OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent,
OAuth2Protocol::OAuth2Protocol_Display => OAuth2Protocol::OAuth2Protocol_Display_Native
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200,
OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent,
OAuth2Protocol::OAuth2Protocol_Display => OAuth2Protocol::OAuth2Protocol_Display_Native
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
@ -120,12 +124,12 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
// do login
$response = $this->action('POST', "UserController@postLogin",
array
(
[
'username' => ' sebastian@tipit.net ',
'password' => ' 1qaz2wsx ',
'_token' => Session::token()
)
'_token' => Session::token(),
'flow' => 'password',
]
);
$this->assertResponseStatus(302);
@ -169,6 +173,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -254,6 +259,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -334,6 +340,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -447,6 +454,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -530,6 +538,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -554,7 +563,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$client_id = '%2E%2D%5F%7E87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client',
$client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg',
$use_enc = true
) {
)
{
$params = array(
@ -592,6 +602,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -701,7 +712,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$client_id = '.-_~87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client',
$client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg',
$use_enc = true
) {
)
{
$params = array(
@ -752,7 +764,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'_token' => Session::token()
'flow' => 'password',
'_token' => Session::token()
)
);
@ -857,7 +870,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
return $access_token;
}
public function testGetRefreshTokenWithPromptSetToConsentLogin(){
public function testGetRefreshTokenWithPromptSetToConsentLogin()
{
$client_id = '.-_~87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client';
$client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg';
@ -871,7 +885,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
OAuth2Protocol::OfflineAccess_Scope),
OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net',
OAuth2Protocol::OAuth2Protocol_Nonce => 'test_nonce',
OAuth2Protocol::OAuth2Protocol_Prompt => sprintf('%s %s',OAuth2Protocol::OAuth2Protocol_Prompt_Login, OAuth2Protocol::OAuth2Protocol_Prompt_Consent),
OAuth2Protocol::OAuth2Protocol_Prompt => sprintf('%s %s', OAuth2Protocol::OAuth2Protocol_Prompt_Login, OAuth2Protocol::OAuth2Protocol_Prompt_Consent),
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200
);
@ -898,6 +912,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1002,7 +1017,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
}
public function testFlowNativeDisplay(){
public function testFlowNativeDisplay()
{
$client_id = '.-_~87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client';
$client_secret = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg';
@ -1014,9 +1030,9 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
'scope' => sprintf('%s profile email address %s', OAuth2Protocol::OpenIdConnect_Scope, OAuth2Protocol::OfflineAccess_Scope),
OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net',
OAuth2Protocol::OAuth2Protocol_Nonce => 'test_nonce',
OAuth2Protocol::OAuth2Protocol_Prompt => sprintf('%s %s',OAuth2Protocol::OAuth2Protocol_Prompt_Login, OAuth2Protocol::OAuth2Protocol_Prompt_Consent),
OAuth2Protocol::OAuth2Protocol_Prompt => sprintf('%s %s', OAuth2Protocol::OAuth2Protocol_Prompt_Login, OAuth2Protocol::OAuth2Protocol_Prompt_Consent),
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200,
OAuth2Protocol::OAuth2Protocol_Display => OAuth2Protocol::OAuth2Protocol_Display_Native
OAuth2Protocol::OAuth2Protocol_Display => OAuth2Protocol::OAuth2Protocol_Display_Native
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
@ -1031,7 +1047,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertResponseStatus(412);
$json_response = json_decode($response->getContent(),true);
$json_response = json_decode($response->getContent(), true);
// do login
$response = $this->call($json_response['method'], $json_response['url'],
@ -1039,6 +1055,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => $json_response['required_params_valid_values']["_token"]
)
);
@ -1057,11 +1074,11 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertResponseStatus(412);
$json_response = json_decode($response->getContent(),true);
$json_response = json_decode($response->getContent(), true);
$response = $this->call($json_response['method'], $json_response['url'], array(
'trust' => 'AllowOnce',
'_token' => $json_response['required_params_valid_values']["_token"]
'_token' => $json_response['required_params_valid_values']["_token"]
));
$this->assertResponseStatus(302);
@ -1117,21 +1134,21 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertTrue(!empty($id_token));
}
public function testGetRefreshTokenFromNativeAppNTimes($n=5)
public function testGetRefreshTokenFromNativeAppNTimes($n = 5)
{
$client_id = 'Jiz87D8/Vcvr6fvQbH4HyNgwKlfSyQ3x.android.openstack.client';
$client_secret = '11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhg11c/6Y5N7kOtGKhgfdfdfdf';
$params_auth_code = array
(
'client_id' => $client_id,
'redirect_uri' => 'androipapp://oidc_endpoint_callback',
'response_type' => 'code',
'scope' => sprintf('%s profile email address %s', OAuth2Protocol::OpenIdConnect_Scope, OAuth2Protocol::OfflineAccess_Scope),
'client_id' => $client_id,
'redirect_uri' => 'androipapp://oidc_endpoint_callback',
'response_type' => 'code',
'scope' => sprintf('%s profile email address %s', OAuth2Protocol::OpenIdConnect_Scope, OAuth2Protocol::OfflineAccess_Scope),
OAuth2Protocol::OAuth2Protocol_LoginHint => 'sebastian@tipit.net',
OAuth2Protocol::OAuth2Protocol_Nonce => 'test_nonce',
OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent,
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200,
OAuth2Protocol::OAuth2Protocol_Nonce => 'test_nonce',
OAuth2Protocol::OAuth2Protocol_Prompt => OAuth2Protocol::OAuth2Protocol_Prompt_Consent,
OAuth2Protocol::OAuth2Protocol_MaxAge => 3200,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
@ -1157,6 +1174,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1280,9 +1298,9 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertTrue(!empty($output['code']));
$params = array(
'code' => $output['code'],
'code' => $output['code'],
'redirect_uri' => 'androipapp://oidc_endpoint_callback',
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
@ -1309,7 +1327,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertTrue(!empty($refresh_token));
$this->assertTrue(!empty($id_token));
++$iteration;
}while( $iteration < $n);
} while ($iteration < $n);
}
public function testTokenResponseModePost()
@ -1354,6 +1372,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1500,6 +1519,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1638,6 +1658,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1816,6 +1837,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -1967,6 +1989,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2047,6 +2070,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2103,7 +2127,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
'client_id' => $client_id,
'redirect_uri' => 'https://www.test.com/oauth2',
'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_IdToken,
'scope' =>join(' ', [
'scope' => join(' ', [
OAuth2Protocol::OpenIdConnect_Scope,
'profile',
'email',
@ -2131,6 +2155,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2177,7 +2202,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
sleep(10);
$params[OAuth2Protocol::OAuth2Protocol_Prompt] = OAuth2Protocol::OAuth2Protocol_Prompt_None;
$params['scope'] =join(' ', [
$params['scope'] = join(' ', [
OAuth2Protocol::OpenIdConnect_Scope,
'profile',
'email',
@ -2240,6 +2265,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2376,6 +2402,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2545,6 +2572,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2651,6 +2679,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2685,8 +2714,8 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$this->assertResponseStatus(302);
$url = $response->getTargetUrl();
$comps = @parse_url($url);
$url = $response->getTargetUrl();
$comps = @parse_url($url);
$fragment = $comps['fragment'];
$this->assertTrue(!empty($fragment));
@ -2792,7 +2821,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
$key->setId('public_key_2');
$alg = new StringOrURI(JSONWebSignatureAndEncryptionAlgorithms::RS512);
$jws = JWSFactory::build( new JWS_ParamsSpecification($key,$alg, $claim_set) );
$jws = JWSFactory::build(new JWS_ParamsSpecification($key, $alg, $claim_set));
// and sign with server private key
$id_token_hint = $jws->toCompactSerialization();
@ -2871,6 +2900,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -2988,6 +3018,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);
@ -3095,6 +3126,7 @@ final class OIDCProtocolTest extends OpenStackIDBaseTest
(
'username' => 'sebastian@tipit.net',
'password' => '1qaz2wsx',
'flow' => 'password',
'_token' => Session::token()
)
);

102
tests/OTPModelTest.php Normal file
View File

@ -0,0 +1,102 @@
<?php namespace Tests;
/**
* Copyright 2021 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\Models\OAuth2\Factories\OTPFactory;
use Illuminate\Support\Facades\App;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Models\OAuth2\Client;
use OAuth2\Factories\OAuth2AuthorizationRequestFactory;
use OAuth2\OAuth2Message;
use OAuth2\OAuth2Protocol;
use Utils\Services\IdentifierGenerator;
/**
* Class OTPModelTest
* @package Tests
*/
class OTPModelTest extends BrowserKitTestCase
{
/**
* @var Client
*/
static $aauth2_client;
protected function setUp():void
{
parent::setUp();
}
protected function tearDown():void
{
parent::tearDown();
}
public function testCreateFromRequest(){
$client_repository = EntityManager::getRepository(Client::class);
$clients = $client_repository->findAll();
$this->assertTrue(count($clients) > 0);
$client = $clients[0];
if(!$client instanceof Client) return;
$client->enablePasswordless();
$client->setOtpLifetime(60 * 3);
$client->setOtpLength(6);
EntityManager::persist($client);
$values =
[
OAuth2Protocol::OAuth2Protocol_ClientId => $client->getClientId(),
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
OAuth2Protocol::OAuth2Protocol_Scope => "test_scope",
OAuth2Protocol::OAuth2Protocol_Nonce => "123456"
];
$request = OAuth2AuthorizationRequestFactory::getInstance()->build
(
new OAuth2Message($values)
);
$this->assertTrue($request->isValid());
$otp = OTPFactory::buildFromRequest($request, App::make(IdentifierGenerator::class), $client);
EntityManager::persist($client);
EntityManager::flush();
$this->assertTrue($client->getOTPGrantsByEmailNotRedeemed("test@test.com")->count() > 0);
$this->assertTrue(strlen($otp->getValue()) == $client->getOtpLength());
}
public function testCreateFromPayloadNoClient(){
$payload =
[
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
OAuth2Protocol::OAuth2Protocol_Scope => "test_scope"
];
$otp = OTPFactory::buildFromPayload($payload, App::make(IdentifierGenerator::class));
EntityManager::persist($otp);
EntityManager::flush();
$this->assertTrue($otp->getId() > 0);
}
}

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -23,6 +23,7 @@ use Illuminate\Support\Facades\Config;
use Models\OpenId\OpenIdTrustedSite;
use OpenId\Extensions\Implementations\OpenIdSREGExtension_1_0;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Illuminate\Support\Facades\Auth;
/**
* Class OpenIdProtocolTest
* Test Suite for OpenId Protocol

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 Openstack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,6 @@
**/
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Tests\BrowserKitTestCase;
/**
* Class OpenStackIDBaseTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -15,7 +15,6 @@ use Models\OAuth2\ResourceServer;
use Illuminate\Support\Facades\Config;
use Auth\User;
use Illuminate\Support\Facades\Session;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
/**
* Class ResourceServerApiTest

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -14,10 +14,9 @@
use OpenId\Services\OpenIdServiceCatalog;
use Utils\Services\IAuthService;
use OpenId\Repositories\IOpenIdTrustedSiteRepository;
use OpenId\Models\IOpenIdUser;
use Auth\User;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Mockery;
/**
* Class TrustedSitesServiceTest
*/

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -11,7 +11,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Tests\TestCase;
use App\Http\Utils\CookieSameSitePolicy;
/**
* Class UserAgentTests

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -12,7 +12,6 @@
* limitations under the License.
**/
use Auth\UserNameGeneratorService;
use Tests\BrowserKitTestCase;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Auth\User;
/**

View File

@ -1,4 +1,4 @@
<?php
<?php namespace Tests;
/**
* Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,7 +13,6 @@
**/
use OpenId\Xrds\XRDSDocumentBuilder;
use OpenId\Xrds\XRDSService;
use Tests\BrowserKitTestCase;
/**
* Class XRDSDocumentTest
*/