mirror of
https://github.com/bitinflow/accounts.git
synced 2026-03-13 13:35:52 +00:00
Add jwt handling, inspired by passport
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"version": "3.1.1",
|
||||
"name": "ghostzero/bitinflow-accounts",
|
||||
"description": "PHP bitinflow Accounts API Client for Laravel 5+",
|
||||
"license": "MIT",
|
||||
@@ -9,7 +10,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"php": "^7.2",
|
||||
"ext-json": "*",
|
||||
"illuminate/support": "^8.0",
|
||||
"illuminate/console": "^8.0",
|
||||
|
||||
82
src/GhostZero/BitinflowAccounts/ApiTokenCookieFactory.php
Normal file
82
src/GhostZero/BitinflowAccounts/ApiTokenCookieFactory.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Contracts\Config\Repository as Config;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
|
||||
class ApiTokenCookieFactory
|
||||
{
|
||||
/**
|
||||
* The configuration repository implementation.
|
||||
*
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The encrypter implementation.
|
||||
*
|
||||
* @var Encrypter
|
||||
*/
|
||||
protected $encrypter;
|
||||
|
||||
/**
|
||||
* Create an API token cookie factory instance.
|
||||
*
|
||||
* @param Config $config
|
||||
* @param Encrypter $encrypter
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Config $config, Encrypter $encrypter)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API token cookie.
|
||||
*
|
||||
* @param mixed $userId
|
||||
* @param string $csrfToken
|
||||
* @return Cookie
|
||||
*/
|
||||
public function make($userId, string $csrfToken): Cookie
|
||||
{
|
||||
$config = $this->config->get('session');
|
||||
|
||||
$expiration = Carbon::now()->addMinutes($config['lifetime']);
|
||||
|
||||
return new Cookie(
|
||||
BitinflowAccounts::cookie(),
|
||||
$this->createToken($userId, $csrfToken, $expiration),
|
||||
$expiration,
|
||||
$config['path'],
|
||||
$config['domain'],
|
||||
$config['secure'],
|
||||
true,
|
||||
false,
|
||||
$config['same_site'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new JWT token for the given user ID and CSRF token.
|
||||
*
|
||||
* @param mixed $userId
|
||||
* @param string $csrfToken
|
||||
* @param Carbon $expiration
|
||||
* @return string
|
||||
*/
|
||||
protected function createToken($userId, string $csrfToken, Carbon $expiration): string
|
||||
{
|
||||
return JWT::encode([
|
||||
'sub' => $userId,
|
||||
'csrf' => $csrfToken,
|
||||
'expiry' => $expiration->getTimestamp(),
|
||||
], $this->encrypter->getKey());
|
||||
}
|
||||
}
|
||||
229
src/GhostZero/BitinflowAccounts/Auth/TokenGuard.php
Normal file
229
src/GhostZero/BitinflowAccounts/Auth/TokenGuard.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Auth;
|
||||
|
||||
use Exception;
|
||||
use Firebase\JWT\JWT;
|
||||
use GhostZero\BitinflowAccounts\BitinflowAccounts;
|
||||
use GhostZero\BitinflowAccounts\Helpers\JwtParser;
|
||||
use GhostZero\BitinflowAccounts\Traits\HasBitinflowTokens;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Auth\GuardHelpers;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Illuminate\Cookie\CookieValuePrefix;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class TokenGuard
|
||||
{
|
||||
use GuardHelpers;
|
||||
|
||||
/**
|
||||
* @var Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var JwtParser
|
||||
*/
|
||||
private $jwtParser;
|
||||
|
||||
public function __construct(UserProvider $provider, Encrypter $encrypter, JwtParser $jwtParser)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
$this->encrypter = $encrypter;
|
||||
$this->jwtParser = $jwtParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user for the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function user(Request $request): ?Authenticatable
|
||||
{
|
||||
if ($request->bearerToken()) {
|
||||
return $this->authenticateViaBearerToken($request);
|
||||
} elseif ($request->cookie(BitinflowAccounts::cookie())) {
|
||||
return $this->authenticateViaCookie($request);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the incoming request via the Bearer token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Authenticatable
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function authenticateViaBearerToken(Request $request): ?Authenticatable
|
||||
{
|
||||
if (!$token = $this->validateRequestViaBearerToken($request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the access token is valid we will retrieve the user according to the user ID
|
||||
// associated with the token. We will use the provider implementation which may
|
||||
// be used to retrieve users from Eloquent. Next, we'll be ready to continue.
|
||||
/** @var Authenticatable|HasBitinflowTokens $user */
|
||||
$user = $this->provider->retrieveById(
|
||||
$request->attributes->get('oauth_user_id') ?: null
|
||||
);
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token ? $user->withBitinflowAccessToken($token) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate and get the incoming request via the Bearer token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return stdClass|null
|
||||
* @throws BindingResolutionException
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function validateRequestViaBearerToken(Request $request): ?stdClass
|
||||
{
|
||||
try {
|
||||
$decoded = $this->jwtParser->decode($request);
|
||||
|
||||
$request->attributes->set('oauth_access_token_id', $decoded->jti);
|
||||
$request->attributes->set('oauth_client_id', $decoded->aud);
|
||||
$request->attributes->set('oauth_client_trusted', $decoded->client->trusted);
|
||||
$request->attributes->set('oauth_user_id', $decoded->sub);
|
||||
$request->attributes->set('oauth_scopes', $decoded->scopes);
|
||||
|
||||
return $decoded;
|
||||
} catch (AuthenticationException $e) {
|
||||
$request->headers->set('Authorization', '', true);
|
||||
|
||||
Container::getInstance()->make(
|
||||
ExceptionHandler::class
|
||||
)->report($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the incoming request via the token cookie.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticateViaCookie(Request $request)
|
||||
{
|
||||
if (!$token = $this->getTokenViaCookie($request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If this user exists, we will return this user and attach a "transient" token to
|
||||
// the user model. The transient token assumes it has all scopes since the user
|
||||
// is physically logged into the application via the application's interface.
|
||||
/** @var Authenticatable|HasBitinflowTokens $user */
|
||||
if ($user = $this->provider->retrieveById($token['sub'])) {
|
||||
return $user->withBitinflowAccessToken((object)['scopes' => '*']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token cookie via the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getTokenViaCookie(Request $request): ?array
|
||||
{
|
||||
// If we need to retrieve the token from the cookie, it'll be encrypted so we must
|
||||
// first decrypt the cookie and then attempt to find the token value within the
|
||||
// database. If we can't decrypt the value we'll bail out with a null return.
|
||||
try {
|
||||
$token = $this->decodeJwtTokenCookie($request);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We will compare the CSRF token in the decoded API token against the CSRF header
|
||||
// sent with the request. If they don't match then this request isn't sent from
|
||||
// a valid source and we won't authenticate the request for further handling.
|
||||
if (!BitinflowAccounts::$ignoreCsrfToken && (!$this->validCsrf($token, $request) ||
|
||||
time() >= $token['expiry'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and decrypt the JWT token cookie.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function decodeJwtTokenCookie(Request $request): array
|
||||
{
|
||||
return (array)JWT::decode(
|
||||
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(BitinflowAccounts::cookie()), BitinflowAccounts::$unserializesCookies)),
|
||||
$this->encrypter->getKey(),
|
||||
['HS256']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the CSRF / header are valid and match.
|
||||
*
|
||||
* @param array $token
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function validCsrf(array $token, Request $request): bool
|
||||
{
|
||||
return isset($token['csrf']) && hash_equals(
|
||||
$token['csrf'], (string)$this->getTokenFromRequest($request)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSRF token from the request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return string
|
||||
*/
|
||||
protected function getTokenFromRequest(Request $request): string
|
||||
{
|
||||
$token = $request->header('X-CSRF-TOKEN');
|
||||
|
||||
if (!$token && $header = $request->header('X-XSRF-TOKEN')) {
|
||||
$token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the cookie contents should be serialized.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function serialized(): bool
|
||||
{
|
||||
return EncryptCookies::serialized('XSRF-TOKEN');
|
||||
}
|
||||
}
|
||||
86
src/GhostZero/BitinflowAccounts/Auth/UserProvider.php
Normal file
86
src/GhostZero/BitinflowAccounts/Auth/UserProvider.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Auth;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider as Base;
|
||||
|
||||
class UserProvider implements Base
|
||||
{
|
||||
/**
|
||||
* The user provider instance.
|
||||
*
|
||||
* @var Base
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* The user provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $providerName;
|
||||
|
||||
/**
|
||||
* Create a new Bitinflow Accounts user provider.
|
||||
*
|
||||
* @param Base $provider
|
||||
* @param string $providerName
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Base $provider, $providerName)
|
||||
{
|
||||
$this->provider = $provider;
|
||||
$this->providerName = $providerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
{
|
||||
return $this->provider->retrieveById($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
return $this->provider->retrieveByToken($identifier, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
$this->provider->updateRememberToken($user, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
return $this->provider->retrieveByCredentials($credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return $this->provider->validateCredentials($user, $credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the user provider.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProviderName()
|
||||
{
|
||||
return $this->providerName;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,27 @@ class BitinflowAccounts
|
||||
|
||||
private static $baseUrl = 'https://accounts.bitinflow.com/api/';
|
||||
|
||||
/**
|
||||
* The name for API token cookies.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $cookie = 'bitinflow_token';
|
||||
|
||||
/**
|
||||
* Indicates if Bitinflow Accounts should ignore incoming CSRF tokens.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $ignoreCsrfToken = false;
|
||||
|
||||
/**
|
||||
* Indicates if Bitinflow Accounts should unserializes cookies.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $unserializesCookies = false;
|
||||
|
||||
/**
|
||||
* Guzzle is used to make http requests.
|
||||
* @var \GuzzleHttp\Client
|
||||
@@ -90,6 +111,49 @@ class BitinflowAccounts
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the name for API token cookies.
|
||||
*
|
||||
* @param string|null $cookie
|
||||
* @return string|static
|
||||
*/
|
||||
public static function cookie($cookie = null)
|
||||
{
|
||||
if (is_null($cookie)) {
|
||||
return static::$cookie;
|
||||
}
|
||||
|
||||
static::$cookie = $cookie;
|
||||
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current user for the application with the given scopes.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable|Traits\HasBitinflowTokens $user
|
||||
* @param array $scopes
|
||||
* @param string $guard
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
public static function actingAs($user, $scopes = [], $guard = 'api')
|
||||
{
|
||||
|
||||
$user->withBitinflowAccessToken((object) [
|
||||
'scopes' => $scopes
|
||||
]);
|
||||
|
||||
if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) {
|
||||
$user->wasRecentlyCreated = false;
|
||||
}
|
||||
|
||||
app('auth')->guard($guard)->setUser($user);
|
||||
|
||||
app('auth')->shouldUse($guard);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $baseUrl
|
||||
*
|
||||
|
||||
41
src/GhostZero/BitinflowAccounts/Helpers/JwtParser.php
Normal file
41
src/GhostZero/BitinflowAccounts/Helpers/JwtParser.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Helpers;
|
||||
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class JwtParser
|
||||
{
|
||||
public const ALLOWED_ALGORITHMS = ['RS256'];
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return stdClass
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function decode(Request $request): stdClass
|
||||
{
|
||||
JWT::$leeway = 60;
|
||||
|
||||
try {
|
||||
return JWT::decode(
|
||||
$request->bearerToken(),
|
||||
$this->getOauthPublicKey(),
|
||||
self::ALLOWED_ALGORITHMS
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
throw (new AuthenticationException());
|
||||
}
|
||||
}
|
||||
|
||||
private function getOauthPublicKey()
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/../../../../oauth-public.key');
|
||||
}
|
||||
}
|
||||
@@ -2,81 +2,31 @@
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use GhostZero\BitinflowAccounts\Exceptions\MissingScopeException;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class CheckClientCredentials
|
||||
class CheckClientCredentials extends CheckCredentials
|
||||
{
|
||||
public const ALLOWED_ALGORITHMS = ['RS256'];
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
*
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, ...$scopes)
|
||||
{
|
||||
JWT::$leeway = 60;
|
||||
|
||||
try {
|
||||
$decoded = JWT::decode(
|
||||
$request->bearerToken(),
|
||||
$this->getOauthPublicKey(),
|
||||
self::ALLOWED_ALGORITHMS
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
|
||||
$request->attributes->set('oauth_access_token_id', $decoded->jti);
|
||||
$request->attributes->set('oauth_client_id', $decoded->aud);
|
||||
$request->attributes->set('oauth_client_trusted', $decoded->client->trusted);
|
||||
$request->attributes->set('oauth_user_id', $decoded->sub);
|
||||
$request->attributes->set('oauth_scopes', $decoded->scopes);
|
||||
|
||||
$this->validateScopes($decoded, $scopes);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function getOauthPublicKey()
|
||||
{
|
||||
return file_get_contents(__DIR__ . '/../../../../../oauth-public.key');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate token credentials.
|
||||
*
|
||||
* @param stdClass $token
|
||||
* @param array $scopes
|
||||
*
|
||||
* @return void
|
||||
* @throws MissingScopeException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function validateScopes(stdClass $token, array $scopes)
|
||||
{
|
||||
if (empty($scopes) || in_array('*', $token->scopes)) {
|
||||
if (in_array('*', $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (in_array($scope, $token->scopes)) {
|
||||
return;
|
||||
if (!in_array($scope, $token->scopes)) {
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
use GhostZero\BitinflowAccounts\Exceptions\MissingScopeException;
|
||||
use stdClass;
|
||||
|
||||
class CheckClientCredentialsForAnyScope extends CheckCredentials
|
||||
{
|
||||
/**
|
||||
* Validate token credentials.
|
||||
*
|
||||
* @param stdClass $token
|
||||
* @param array $scopes
|
||||
*
|
||||
* @return void
|
||||
* @throws MissingScopeException
|
||||
*
|
||||
*/
|
||||
protected function validateScopes(stdClass $token, array $scopes)
|
||||
{
|
||||
if (in_array('*', $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (in_array($scope, $token->scopes)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use GhostZero\BitinflowAccounts\Exceptions\MissingScopeException;
|
||||
use GhostZero\BitinflowAccounts\Helpers\JwtParser;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use stdClass;
|
||||
|
||||
abstract class CheckCredentials
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
*
|
||||
* @return mixed
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*
|
||||
*/
|
||||
public function handle($request, Closure $next, ...$scopes)
|
||||
{
|
||||
$decoded = $this->getJwtParser()->decode($request);
|
||||
|
||||
$request->attributes->set('oauth_access_token_id', $decoded->jti);
|
||||
$request->attributes->set('oauth_client_id', $decoded->aud);
|
||||
//$request->attributes->set('oauth_client_trusted', $decoded->client->trusted);
|
||||
$request->attributes->set('oauth_user_id', $decoded->sub);
|
||||
$request->attributes->set('oauth_scopes', $decoded->scopes);
|
||||
|
||||
$this->validateScopes($decoded, $scopes);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function getJwtParser(): JwtParser
|
||||
{
|
||||
return app(JwtParser::class);
|
||||
}
|
||||
|
||||
abstract protected function validateScopes(stdClass $token, array $scopes);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
|
||||
use Closure;
|
||||
use GhostZero\BitinflowAccounts\Exceptions\MissingScopeException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CheckForAnyScope
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
* @return Response
|
||||
*
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*/
|
||||
public function handle($request, $next, ...$scopes)
|
||||
{
|
||||
if (! $request->user() || ! $request->user()->bitinflowToken()) {
|
||||
throw new AuthenticationException;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if ($request->user()->bitinflowTokenCan($scope)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MissingScopeException($scopes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use GhostZero\BitinflowAccounts\Exceptions\MissingScopeException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CheckScopes
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param mixed ...$scopes
|
||||
* @return Response
|
||||
*
|
||||
* @throws AuthenticationException|MissingScopeException
|
||||
*/
|
||||
public function handle($request, $next, ...$scopes)
|
||||
{
|
||||
if (! $request->user() || ! $request->user()->bitinflowToken()) {
|
||||
throw new AuthenticationException;
|
||||
}
|
||||
|
||||
foreach ($scopes as $scope) {
|
||||
if (! $request->user()->bitinflowTokenCan($scope)) {
|
||||
throw new MissingScopeException($scope);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use GhostZero\BitinflowAccounts\ApiTokenCookieFactory;
|
||||
use GhostZero\BitinflowAccounts\BitinflowAccounts;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class CreateFreshApiToken
|
||||
{
|
||||
/**
|
||||
* The API token cookie factory instance.
|
||||
*
|
||||
* @var ApiTokenCookieFactory
|
||||
*/
|
||||
protected $cookieFactory;
|
||||
|
||||
/**
|
||||
* The authentication guard.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $guard;
|
||||
|
||||
/**
|
||||
* Create a new middleware instance.
|
||||
*
|
||||
* @param ApiTokenCookieFactory $cookieFactory
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ApiTokenCookieFactory $cookieFactory)
|
||||
{
|
||||
$this->cookieFactory = $cookieFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @param string|null $guard
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
$this->guard = $guard;
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
if ($this->shouldReceiveFreshToken($request, $response)) {
|
||||
$response->withCookie($this->cookieFactory->make(
|
||||
$request->user($this->guard)->getAuthIdentifier(), $request->session()->token()
|
||||
));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given request should receive a fresh token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function shouldReceiveFreshToken($request, $response): bool
|
||||
{
|
||||
return $this->requestShouldReceiveFreshToken($request) &&
|
||||
$this->responseShouldReceiveFreshToken($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request should receive a fresh token.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function requestShouldReceiveFreshToken($request)
|
||||
{
|
||||
return $request->isMethod('GET') && $request->user($this->guard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response should receive a fresh token.
|
||||
*
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function responseShouldReceiveFreshToken($response)
|
||||
{
|
||||
return ($response instanceof Response ||
|
||||
$response instanceof JsonResponse) &&
|
||||
! $this->alreadyContainsToken($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given response already contains an API token.
|
||||
*
|
||||
* This avoids us overwriting a just "refreshed" token.
|
||||
*
|
||||
* @param Response $response
|
||||
* @return bool
|
||||
*/
|
||||
protected function alreadyContainsToken($response): bool
|
||||
{
|
||||
foreach ($response->headers->getCookies() as $cookie) {
|
||||
if ($cookie->getName() === BitinflowAccounts::cookie()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Providers;
|
||||
|
||||
use GhostZero\BitinflowAccounts\Auth\TokenGuard;
|
||||
use GhostZero\BitinflowAccounts\Auth\UserProvider;
|
||||
use GhostZero\BitinflowAccounts\BitinflowAccounts;
|
||||
use GhostZero\BitinflowAccounts\Helpers\JwtParser;
|
||||
use Illuminate\Auth\RequestGuard;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BitinflowAccountsServiceProvider extends ServiceProvider
|
||||
@@ -33,6 +38,8 @@ class BitinflowAccountsServiceProvider extends ServiceProvider
|
||||
$this->app->singleton(BitinflowAccounts::class, function () {
|
||||
return new BitinflowAccounts;
|
||||
});
|
||||
|
||||
$this->registerGuard();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,4 +50,37 @@ class BitinflowAccountsServiceProvider extends ServiceProvider
|
||||
{
|
||||
return [BitinflowAccounts::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the token guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerGuard()
|
||||
{
|
||||
Auth::resolved(function ($auth) {
|
||||
$auth->extend('bitinflow-accounts', function ($app, $name, array $config) {
|
||||
return tap($this->makeGuard($config), function ($guard) {
|
||||
$this->app->refresh('request', $guard, 'setRequest');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an instance of the token guard.
|
||||
*
|
||||
* @param array $config
|
||||
* @return RequestGuard
|
||||
*/
|
||||
protected function makeGuard(array $config): RequestGuard
|
||||
{
|
||||
return new RequestGuard(function ($request) use ($config) {
|
||||
return (new TokenGuard(
|
||||
new UserProvider(Auth::createUserProvider($config['provider']), $config['provider']),
|
||||
$this->app->make('encrypter'),
|
||||
$this->app->make(JwtParser::class)
|
||||
))->user($request);
|
||||
}, $this->app['request']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace GhostZero\BitinflowAccounts\Traits;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use stdClass;
|
||||
|
||||
trait HasBitinflowTokens
|
||||
{
|
||||
/**
|
||||
* The current access token for the authentication user.
|
||||
*
|
||||
* @var stdClass
|
||||
*/
|
||||
protected $accessToken;
|
||||
|
||||
/**
|
||||
* Get the current access token being used by the user.
|
||||
*
|
||||
* @return stdClass|null
|
||||
*/
|
||||
public function bitinflowToken(): ?stdClass
|
||||
{
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current API token has a given scope.
|
||||
*
|
||||
* @param string $scopeUserProvider
|
||||
* @return bool
|
||||
*/
|
||||
public function bitinflowTokenCan(string $scope): bool
|
||||
{
|
||||
return $this->accessToken ? in_array($scope, $this->accessToken->scopes) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current access token for the user.
|
||||
*
|
||||
* @param stdClass $accessToken
|
||||
* @return $this
|
||||
*/
|
||||
public function withBitinflowAccessToken(stdClass $accessToken): self
|
||||
{
|
||||
$this->accessToken = $accessToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user