Passwordlress Flow (WIP)

* API endpoints for embedded login flow
* unit tests

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 75fc12a564
commit ce9b9a8698
76 changed files with 3381 additions and 561 deletions

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 = 1;
/**
* @var string
*/
public $otp;
/**
* @var string
*/
public $to;
/**
* @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->to = $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->to));
return $this->from(Config::get("mail.from"))
->to($this->to)
->subject($subject)
->view('emails.oauth2_passwordless_otp');
}
}

View File

@ -90,6 +90,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 +361,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 +382,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 +398,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 +424,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 = [
@ -1603,4 +1629,97 @@ 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->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_None;
}
public function disablePasswordless(): void
{
$this->otp_enabled = false;
}
/**
* @return int
*/
public function getOtpLength(): int
{
return $this->otp_length;
}
/**
* @param int $otp_length
*/
public function setOtpLength(int $otp_length): void
{
$this->otp_length = $otp_length;
}
/**
* @return int
*/
public function getOtpLifetime(): int
{
return $this->otp_lifetime;
}
/**
* @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

@ -0,0 +1,95 @@
<?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->setScopes($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
* @return OAuth2OTP
*/
static public function buildFromPayload(array $payload, IdentifierGenerator $identifier_generator):OAuth2OTP{
$lifetime = Config::get("otp.lifetime", 120);
$length = Config::get("otp.length",6);
$otp = new OAuth2OTP($length, $lifetime);
$otp->setConnection($payload[OAuth2Protocol::OAuth2PasswordlessConnection]);
$otp->setSend($payload[OAuth2Protocol::OAuth2PasswordlessSend]);
$otp->setScopes($payload[OAuth2Protocol::OAuth2Protocol_Scope]);
$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);
return $otp;
}
}

View File

@ -0,0 +1,450 @@
<?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="scopes", type="string")
* @var string
*/
private $scopes;
/**
* @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 getScopes(): string
{
return $this->scopes;
}
/**
* @param string $scopes
*/
public function setScopes(string $scopes): void
{
$this->scopes = $scopes;
}
/**
* @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->scopes);
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->scopes = $request->getScopes();
$instance->value = $request->getOTP();
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

@ -0,0 +1,66 @@
<?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();
}
}

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

@ -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,12 @@
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\User;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
@ -23,6 +28,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 +56,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 +75,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 +109,10 @@ final class TokenService extends AbstractService implements ITokenService
* @var IClientService
*/
private $client_service;
/**
* @var IUserService
*/
private $user_service;
/**
* @var ILockManagerService
*/
@ -123,15 +136,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 +192,35 @@ 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 IOAuth2OTPRepository $otp_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,157 @@ final class TokenService extends AbstractService implements ITokenService
return $this->getAccessToken($db_access_token->getValue(), true);
}
/**
* @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){
$otp = OTPFactory::buildFromRequest($request, $this->identifier_generator, $client);
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 OAuth2OTP $otp
* @param Client|null $client
* @return AccessToken
* @throws Exception
*/
public function createAccessTokenFromOTP(OAuth2OTP $otp, ?Client $client): AccessToken
{
// build current audience ...
$audience = $this->scope_service->getStrAudienceByScopeNames
(
explode
(
OAuth2Protocol::OAuth2Protocol_Scope_Delimiter,
$otp->getScopes()
)
);
$access_token = $this->identifier_generator->generate
(
AccessToken::createFromOTP
(
$otp,
! is_null($client) ? $client->getClientId() : null,
$audience,
$this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime')
)
);
$db_otp = $this->tx_service->transaction(function() use($otp, $client){
// find first db OTP by connection , by username (email/phone) number and client not redeemed
$db_otp = $this->otp_repository->getByConnectionAndUserNameNotRedeemed
(
$otp->getConnection(),
$otp->getUserName(),
$client
);
if(is_null($db_otp)){
// otp no emitted
throw new InvalidOTPException("Non existent OTP.");
}
$db_otp->logRedeemAttempt();
return $db_otp;
});
return $this->tx_service->transaction(function() use($otp, $db_otp, $client, $access_token){
if( $db_otp->getValue() != $otp->getValue() ||
$db_otp->getConnection() != $otp->getConnection() ||
!$db_otp->isValid() ||
($db_otp->hasClient() && is_null($client)) ||
($db_otp->hasClient() && !is_null($client) && $client->getClientId() != $db_otp->getClient()->getClientId()) ||
($db_otp->getUserName() != $otp->getUserName()) ||
(!$db_otp->allowScope($otp->getScopes()))
){
if(!$db_otp->isValid() || !$db_otp->allowScope($otp->getScopes()))
throw new InvalidOTPException("Invalidated OTP.");
throw new InvalidOTPException("Non existent OTP.");
}
// we have a valid OTP
$user = $this->auth_service->getUserByUsername($otp->getUserName());
if(is_null($user))// we need to create a new one ( auto register)
{
Log::debug(sprintf("TokenService::createAccessTokenFromOTP user %s does not exists ...", $otp->getUserName()));
$user = $this->user_service->registerUser([
'email' => $otp->getUserName(),
'email_verified' => true,
]);
}
$otp->setAuthTime(time());
$otp->setUserId($user->getId());
$otp->setNonce($db_otp->getNonce());
$db_otp->redeem();
// 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($user);
$this->access_token_repository->add($access_token_db);
//check if use refresh tokens...
if
(
$client->useRefreshToken() &&
$client->isPasswordlessEnabled() &&
str_contains($otp->getScopes(), 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;
});
}
}

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

@ -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

@ -0,0 +1,373 @@
<?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 &&
$request->isValid() &&
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->isValid() &&
$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->getScopes()
);
}
/**
* @param OAuth2Request $request
* @return OAuth2Response
* @throws \Exception
*/
public function handle(OAuth2Request $request)
{
try {
if (!($request instanceof OAuth2PasswordlessAuthenticationRequest)) {
throw new InvalidOAuth2Request;
}
$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;
}
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->getScopes();
// 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,42 @@
<?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;
}

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,
];
/**
* @var array
*/
public static $optional_params = [
OAuth2Protocol::OAuth2Protocol_Nonce => [],
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,23 @@ 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 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()));

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

@ -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,94 @@
<?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->string("scopes")->setNotnull(true);
$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(false)->setDefault(6);
// seconds
$table->integer('lifetime')->setNotnull(true);
$table->dateTime('redeemed_at')->setNotnull(false);
// FK Optional
$table->bigInteger("oauth2_client_id", false, 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

@ -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

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
</head>
<body>
<p>Dear User</p>
<p>Here is your Verification code <b>{{$otp}}</b>.</p>
<p>Should be valid for {{$lifetime}} minutes.</p>
<br/>
<br/>
<p>Cheers,<br/>Your {!! Config::get('app.tenant_name') !!} Support Team</p>
</body>
</html><?php

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");
@ -35,6 +35,8 @@ 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 +51,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();
}

101
tests/OTPModelTest.php Normal file
View File

@ -0,0 +1,101 @@
<?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"
];
$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
*/