mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-23 11:22:33 +01:00
Renamed OIDC files to all be aligned
This commit is contained in:
parent
06a0d829c8
commit
c167f40af3
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use League\OAuth2\Client\Token\AccessToken;
|
use League\OAuth2\Client\Token\AccessToken;
|
||||||
|
|
||||||
class OpenIdConnectAccessToken extends AccessToken
|
class OidcAccessToken extends AccessToken
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Constructs an access token.
|
* Constructs an access token.
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
class OpenIdConnectIdToken
|
class OidcIdToken
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var array
|
* @var array
|
||||||
@ -74,7 +74,7 @@ class OpenIdConnectIdToken
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate all possible parts of the id token.
|
* Validate all possible parts of the id token.
|
||||||
* @throws InvalidTokenException
|
* @throws OidcInvalidTokenException
|
||||||
*/
|
*/
|
||||||
public function validate(string $clientId): bool
|
public function validate(string $clientId): bool
|
||||||
{
|
{
|
||||||
@ -105,35 +105,35 @@ class OpenIdConnectIdToken
|
|||||||
/**
|
/**
|
||||||
* Validate the structure of the given token and ensure we have the required pieces.
|
* Validate the structure of the given token and ensure we have the required pieces.
|
||||||
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2
|
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2
|
||||||
* @throws InvalidTokenException
|
* @throws OidcInvalidTokenException
|
||||||
*/
|
*/
|
||||||
protected function validateTokenStructure(): void
|
protected function validateTokenStructure(): void
|
||||||
{
|
{
|
||||||
foreach (['header', 'payload'] as $prop) {
|
foreach (['header', 'payload'] as $prop) {
|
||||||
if (empty($this->$prop) || !is_array($this->$prop)) {
|
if (empty($this->$prop) || !is_array($this->$prop)) {
|
||||||
throw new InvalidTokenException("Could not parse out a valid {$prop} within the provided token");
|
throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($this->signature) || !is_string($this->signature)) {
|
if (empty($this->signature) || !is_string($this->signature)) {
|
||||||
throw new InvalidTokenException("Could not parse out a valid signature within the provided token");
|
throw new OidcInvalidTokenException("Could not parse out a valid signature within the provided token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the signature of the given token and ensure it validates against the provided key.
|
* Validate the signature of the given token and ensure it validates against the provided key.
|
||||||
* @throws InvalidTokenException
|
* @throws OidcInvalidTokenException
|
||||||
*/
|
*/
|
||||||
protected function validateTokenSignature(): void
|
protected function validateTokenSignature(): void
|
||||||
{
|
{
|
||||||
if ($this->header['alg'] !== 'RS256') {
|
if ($this->header['alg'] !== 'RS256') {
|
||||||
throw new InvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}");
|
throw new OidcInvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$parsedKeys = array_map(function($key) {
|
$parsedKeys = array_map(function($key) {
|
||||||
try {
|
try {
|
||||||
return new JwtSigningKey($key);
|
return new OidcJwtSigningKey($key);
|
||||||
} catch (InvalidKeyException $e) {
|
} catch (OidcInvalidKeyException $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, $this->keys);
|
}, $this->keys);
|
||||||
@ -141,27 +141,27 @@ class OpenIdConnectIdToken
|
|||||||
$parsedKeys = array_filter($parsedKeys);
|
$parsedKeys = array_filter($parsedKeys);
|
||||||
|
|
||||||
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
||||||
/** @var JwtSigningKey $parsedKey */
|
/** @var OidcJwtSigningKey $parsedKey */
|
||||||
foreach ($parsedKeys as $parsedKey) {
|
foreach ($parsedKeys as $parsedKey) {
|
||||||
if ($parsedKey->verify($contentToSign, $this->signature)) {
|
if ($parsedKey->verify($contentToSign, $this->signature)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidTokenException('Token signature could not be validated using the provided keys');
|
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the claims of the token.
|
* Validate the claims of the token.
|
||||||
* As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
|
* As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
|
||||||
* @throws InvalidTokenException
|
* @throws OidcInvalidTokenException
|
||||||
*/
|
*/
|
||||||
protected function validateTokenClaims(string $clientId): void
|
protected function validateTokenClaims(string $clientId): void
|
||||||
{
|
{
|
||||||
// 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
|
// 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
|
||||||
// MUST exactly match the value of the iss (issuer) Claim.
|
// MUST exactly match the value of the iss (issuer) Claim.
|
||||||
if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) {
|
if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) {
|
||||||
throw new InvalidTokenException('Missing or non-matching token issuer value');
|
throw new OidcInvalidTokenException('Missing or non-matching token issuer value');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
|
// 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
|
||||||
@ -169,16 +169,16 @@ class OpenIdConnectIdToken
|
|||||||
// if the ID Token does not list the Client as a valid audience, or if it contains additional
|
// if the ID Token does not list the Client as a valid audience, or if it contains additional
|
||||||
// audiences not trusted by the Client.
|
// audiences not trusted by the Client.
|
||||||
if (empty($this->payload['aud'])) {
|
if (empty($this->payload['aud'])) {
|
||||||
throw new InvalidTokenException('Missing token audience value');
|
throw new OidcInvalidTokenException('Missing token audience value');
|
||||||
}
|
}
|
||||||
|
|
||||||
$aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
|
$aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
|
||||||
if (count($aud) !== 1) {
|
if (count($aud) !== 1) {
|
||||||
throw new InvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');
|
throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($aud[0] !== $clientId) {
|
if ($aud[0] !== $clientId) {
|
||||||
throw new InvalidTokenException('Token audience value did not match the expected client_id');
|
throw new OidcInvalidTokenException('Token audience value did not match the expected client_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
|
// 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
|
||||||
@ -187,32 +187,32 @@ class OpenIdConnectIdToken
|
|||||||
// 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id
|
// 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id
|
||||||
// is the Claim Value.
|
// is the Claim Value.
|
||||||
if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {
|
if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {
|
||||||
throw new InvalidTokenException('Token authorized party exists but does not match the expected client_id');
|
throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. The current time MUST be before the time represented by the exp Claim
|
// 5. The current time MUST be before the time represented by the exp Claim
|
||||||
// (possibly allowing for some small leeway to account for clock skew).
|
// (possibly allowing for some small leeway to account for clock skew).
|
||||||
if (empty($this->payload['exp'])) {
|
if (empty($this->payload['exp'])) {
|
||||||
throw new InvalidTokenException('Missing token expiration time value');
|
throw new OidcInvalidTokenException('Missing token expiration time value');
|
||||||
}
|
}
|
||||||
|
|
||||||
$skewSeconds = 120;
|
$skewSeconds = 120;
|
||||||
$now = time();
|
$now = time();
|
||||||
if ($now >= (intval($this->payload['exp']) + $skewSeconds)) {
|
if ($now >= (intval($this->payload['exp']) + $skewSeconds)) {
|
||||||
throw new InvalidTokenException('Token has expired');
|
throw new OidcInvalidTokenException('Token has expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. The iat Claim can be used to reject tokens that were issued too far away from the current time,
|
// 6. The iat Claim can be used to reject tokens that were issued too far away from the current time,
|
||||||
// limiting the amount of time that nonces need to be stored to prevent attacks.
|
// limiting the amount of time that nonces need to be stored to prevent attacks.
|
||||||
// The acceptable range is Client specific.
|
// The acceptable range is Client specific.
|
||||||
if (empty($this->payload['iat'])) {
|
if (empty($this->payload['iat'])) {
|
||||||
throw new InvalidTokenException('Missing token issued at time value');
|
throw new OidcInvalidTokenException('Missing token issued at time value');
|
||||||
}
|
}
|
||||||
|
|
||||||
$dayAgo = time() - 86400;
|
$dayAgo = time() - 86400;
|
||||||
$iat = intval($this->payload['iat']);
|
$iat = intval($this->payload['iat']);
|
||||||
if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) {
|
if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) {
|
||||||
throw new InvalidTokenException('Token issue at time is not recent or is invalid');
|
throw new OidcInvalidTokenException('Token issue at time is not recent or is invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
|
// 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
|
||||||
@ -225,7 +225,7 @@ class OpenIdConnectIdToken
|
|||||||
|
|
||||||
// Custom: Ensure the "sub" (Subject) Claim exists and has a value.
|
// Custom: Ensure the "sub" (Subject) Claim exists and has a value.
|
||||||
if (empty($this->payload['sub'])) {
|
if (empty($this->payload['sub'])) {
|
||||||
throw new InvalidTokenException('Missing token subject value');
|
throw new OidcInvalidTokenException('Missing token subject value');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
app/Auth/Access/Oidc/OidcInvalidKeyException.php
Normal file
8
app/Auth/Access/Oidc/OidcInvalidKeyException.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
|
class OidcInvalidKeyException extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
10
app/Auth/Access/Oidc/OidcInvalidTokenException.php
Normal file
10
app/Auth/Access/Oidc/OidcInvalidTokenException.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class OidcInvalidTokenException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
8
app/Auth/Access/Oidc/OidcIssuerDiscoveryException.php
Normal file
8
app/Auth/Access/Oidc/OidcIssuerDiscoveryException.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
|
class OidcIssuerDiscoveryException extends \Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
use phpseclib3\Crypt\Common\PublicKey;
|
use phpseclib3\Crypt\Common\PublicKey;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
use phpseclib3\Crypt\RSA;
|
use phpseclib3\Crypt\RSA;
|
||||||
use phpseclib3\Math\BigInteger;
|
use phpseclib3\Math\BigInteger;
|
||||||
|
|
||||||
class JwtSigningKey
|
class OidcJwtSigningKey
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var PublicKey
|
* @var PublicKey
|
||||||
@ -20,7 +20,7 @@ class JwtSigningKey
|
|||||||
* 'file:///var/www/cert.pem'
|
* 'file:///var/www/cert.pem'
|
||||||
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...']
|
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...']
|
||||||
* @param array|string $jwkOrKeyPath
|
* @param array|string $jwkOrKeyPath
|
||||||
* @throws InvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
public function __construct($jwkOrKeyPath)
|
public function __construct($jwkOrKeyPath)
|
||||||
{
|
{
|
||||||
@ -29,12 +29,12 @@ class JwtSigningKey
|
|||||||
} else if (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
|
} else if (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
|
||||||
$this->loadFromPath($jwkOrKeyPath);
|
$this->loadFromPath($jwkOrKeyPath);
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidKeyException('Unexpected type of key value provided');
|
throw new OidcInvalidKeyException('Unexpected type of key value provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
protected function loadFromPath(string $path)
|
protected function loadFromPath(string $path)
|
||||||
{
|
{
|
||||||
@ -43,37 +43,37 @@ class JwtSigningKey
|
|||||||
file_get_contents($path)
|
file_get_contents($path)
|
||||||
)->withPadding(RSA::SIGNATURE_PKCS1);
|
)->withPadding(RSA::SIGNATURE_PKCS1);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new InvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
|
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($this->key instanceof RSA)) {
|
if (!($this->key instanceof RSA)) {
|
||||||
throw new InvalidKeyException("Key loaded from file path is not an RSA key as expected");
|
throw new OidcInvalidKeyException("Key loaded from file path is not an RSA key as expected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidKeyException
|
* @throws OidcInvalidKeyException
|
||||||
*/
|
*/
|
||||||
protected function loadFromJwkArray(array $jwk)
|
protected function loadFromJwkArray(array $jwk)
|
||||||
{
|
{
|
||||||
if ($jwk['alg'] !== 'RS256') {
|
if ($jwk['alg'] !== 'RS256') {
|
||||||
throw new InvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
|
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($jwk['use'])) {
|
if (empty($jwk['use'])) {
|
||||||
throw new InvalidKeyException('A "use" parameter on the provided key is expected');
|
throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($jwk['use'] !== 'sig') {
|
if ($jwk['use'] !== 'sig') {
|
||||||
throw new InvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
|
throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($jwk['e'])) {
|
if (empty($jwk['e'])) {
|
||||||
throw new InvalidKeyException('An "e" parameter on the provided key is expected');
|
throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($jwk['n'])) {
|
if (empty($jwk['n'])) {
|
||||||
throw new InvalidKeyException('A "n" parameter on the provided key is expected');
|
throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
|
||||||
}
|
}
|
||||||
|
|
||||||
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
||||||
@ -85,7 +85,7 @@ class JwtSigningKey
|
|||||||
'n' => new BigInteger(base64_decode($n), 256),
|
'n' => new BigInteger(base64_decode($n), 256),
|
||||||
])->withPadding(RSA::SIGNATURE_PKCS1);
|
])->withPadding(RSA::SIGNATURE_PKCS1);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new InvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
|
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
use League\OAuth2\Client\Grant\AbstractGrant;
|
use League\OAuth2\Client\Grant\AbstractGrant;
|
||||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||||
@ -16,7 +16,7 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
* Credit to the https://github.com/steverhoades/oauth2-openid-connect-client
|
* Credit to the https://github.com/steverhoades/oauth2-openid-connect-client
|
||||||
* project for the idea of extending a League\OAuth2 client for this use-case.
|
* project for the idea of extending a League\OAuth2 client for this use-case.
|
||||||
*/
|
*/
|
||||||
class OpenIdConnectOAuthProvider extends AbstractProvider
|
class OidcOAuthProvider extends AbstractProvider
|
||||||
{
|
{
|
||||||
use BearerAuthorizationTrait;
|
use BearerAuthorizationTrait;
|
||||||
|
|
||||||
@ -116,11 +116,11 @@ class OpenIdConnectOAuthProvider extends AbstractProvider
|
|||||||
*
|
*
|
||||||
* @param array $response
|
* @param array $response
|
||||||
* @param AbstractGrant $grant
|
* @param AbstractGrant $grant
|
||||||
* @return OpenIdConnectAccessToken
|
* @return OidcAccessToken
|
||||||
*/
|
*/
|
||||||
protected function createAccessToken(array $response, AbstractGrant $grant)
|
protected function createAccessToken(array $response, AbstractGrant $grant)
|
||||||
{
|
{
|
||||||
return new OpenIdConnectAccessToken($response);
|
return new OidcAccessToken($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
@ -13,7 +13,7 @@ use Psr\Http\Client\ClientInterface;
|
|||||||
* Acts as a DTO for settings used within the oidc request and token handling.
|
* Acts as a DTO for settings used within the oidc request and token handling.
|
||||||
* Performs auto-discovery upon request.
|
* Performs auto-discovery upon request.
|
||||||
*/
|
*/
|
||||||
class OpenIdConnectProviderSettings
|
class OidcProviderSettings
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
@ -103,7 +103,7 @@ class OpenIdConnectProviderSettings
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover and autoload settings from the configured issuer.
|
* Discover and autoload settings from the configured issuer.
|
||||||
* @throws IssuerDiscoveryException
|
* @throws OidcIssuerDiscoveryException
|
||||||
*/
|
*/
|
||||||
public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
|
public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
|
||||||
{
|
{
|
||||||
@ -114,12 +114,12 @@ class OpenIdConnectProviderSettings
|
|||||||
});
|
});
|
||||||
$this->applySettingsFromArray($discoveredSettings);
|
$this->applySettingsFromArray($discoveredSettings);
|
||||||
} catch (ClientExceptionInterface $exception) {
|
} catch (ClientExceptionInterface $exception) {
|
||||||
throw new IssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
|
throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IssuerDiscoveryException
|
* @throws OidcIssuerDiscoveryException
|
||||||
* @throws ClientExceptionInterface
|
* @throws ClientExceptionInterface
|
||||||
*/
|
*/
|
||||||
protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
|
protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
|
||||||
@ -130,11 +130,11 @@ class OpenIdConnectProviderSettings
|
|||||||
$result = json_decode($response->getBody()->getContents(), true);
|
$result = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
if (empty($result) || !is_array($result)) {
|
if (empty($result) || !is_array($result)) {
|
||||||
throw new IssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
|
throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result['issuer'] !== $this->issuer) {
|
if ($result['issuer'] !== $this->issuer) {
|
||||||
throw new IssuerDiscoveryException("Unexpected issuer value found on discovery response");
|
throw new OidcIssuerDiscoveryException("Unexpected issuer value found on discovery response");
|
||||||
}
|
}
|
||||||
|
|
||||||
$discoveredSettings = [];
|
$discoveredSettings = [];
|
||||||
@ -168,7 +168,7 @@ class OpenIdConnectProviderSettings
|
|||||||
/**
|
/**
|
||||||
* Return an array of jwks as PHP key=>value arrays.
|
* Return an array of jwks as PHP key=>value arrays.
|
||||||
* @throws ClientExceptionInterface
|
* @throws ClientExceptionInterface
|
||||||
* @throws IssuerDiscoveryException
|
* @throws OidcIssuerDiscoveryException
|
||||||
*/
|
*/
|
||||||
protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
|
protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
|
||||||
{
|
{
|
||||||
@ -177,7 +177,7 @@ class OpenIdConnectProviderSettings
|
|||||||
$result = json_decode($response->getBody()->getContents(), true);
|
$result = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
if (empty($result) || !is_array($result) || !isset($result['keys'])) {
|
if (empty($result) || !is_array($result) || !isset($result['keys'])) {
|
||||||
throw new IssuerDiscoveryException("Error reading keys from issuer jwks_uri");
|
throw new OidcIssuerDiscoveryException("Error reading keys from issuer jwks_uri");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result['keys'];
|
return $result['keys'];
|
@ -1,4 +1,4 @@
|
|||||||
<?php namespace BookStack\Auth\Access\OpenIdConnect;
|
<?php namespace BookStack\Auth\Access\Oidc;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Auth\Access\LoginService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Auth\Access\RegistrationService;
|
||||||
@ -20,7 +20,7 @@ use function url;
|
|||||||
* Class OpenIdConnectService
|
* Class OpenIdConnectService
|
||||||
* Handles any app-specific OIDC tasks.
|
* Handles any app-specific OIDC tasks.
|
||||||
*/
|
*/
|
||||||
class OpenIdConnectService
|
class OidcService
|
||||||
{
|
{
|
||||||
protected $registrationService;
|
protected $registrationService;
|
||||||
protected $loginService;
|
protected $loginService;
|
||||||
@ -72,12 +72,12 @@ class OpenIdConnectService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IssuerDiscoveryException
|
* @throws OidcIssuerDiscoveryException
|
||||||
* @throws ClientExceptionInterface
|
* @throws ClientExceptionInterface
|
||||||
*/
|
*/
|
||||||
protected function getProviderSettings(): OpenIdConnectProviderSettings
|
protected function getProviderSettings(): OidcProviderSettings
|
||||||
{
|
{
|
||||||
$settings = new OpenIdConnectProviderSettings([
|
$settings = new OidcProviderSettings([
|
||||||
'issuer' => $this->config['issuer'],
|
'issuer' => $this->config['issuer'],
|
||||||
'clientId' => $this->config['client_id'],
|
'clientId' => $this->config['client_id'],
|
||||||
'clientSecret' => $this->config['client_secret'],
|
'clientSecret' => $this->config['client_secret'],
|
||||||
@ -104,15 +104,15 @@ class OpenIdConnectService
|
|||||||
/**
|
/**
|
||||||
* Load the underlying OpenID Connect Provider.
|
* Load the underlying OpenID Connect Provider.
|
||||||
*/
|
*/
|
||||||
protected function getProvider(OpenIdConnectProviderSettings $settings): OpenIdConnectOAuthProvider
|
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
|
||||||
{
|
{
|
||||||
return new OpenIdConnectOAuthProvider($settings->arrayForProvider());
|
return new OidcOAuthProvider($settings->arrayForProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the display name
|
* Calculate the display name
|
||||||
*/
|
*/
|
||||||
protected function getUserDisplayName(OpenIdConnectIdToken $token, string $defaultValue): string
|
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
|
||||||
{
|
{
|
||||||
$displayNameAttr = $this->config['display_name_claims'];
|
$displayNameAttr = $this->config['display_name_claims'];
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ class OpenIdConnectService
|
|||||||
* Extract the details of a user from an ID token.
|
* Extract the details of a user from an ID token.
|
||||||
* @return array{name: string, email: string, external_id: string}
|
* @return array{name: string, email: string, external_id: string}
|
||||||
*/
|
*/
|
||||||
protected function getUserDetails(OpenIdConnectIdToken $token): array
|
protected function getUserDetails(OidcIdToken $token): array
|
||||||
{
|
{
|
||||||
$id = $token->getClaim('sub');
|
$id = $token->getClaim('sub');
|
||||||
return [
|
return [
|
||||||
@ -153,10 +153,10 @@ class OpenIdConnectService
|
|||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
* @throws StoppedAuthenticationException
|
* @throws StoppedAuthenticationException
|
||||||
*/
|
*/
|
||||||
protected function processAccessTokenCallback(OpenIdConnectAccessToken $accessToken, OpenIdConnectProviderSettings $settings): User
|
protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User
|
||||||
{
|
{
|
||||||
$idTokenText = $accessToken->getIdToken();
|
$idTokenText = $accessToken->getIdToken();
|
||||||
$idToken = new OpenIdConnectIdToken(
|
$idToken = new OidcIdToken(
|
||||||
$idTokenText,
|
$idTokenText,
|
||||||
$settings->issuer,
|
$settings->issuer,
|
||||||
$settings->keys,
|
$settings->keys,
|
||||||
@ -168,7 +168,7 @@ class OpenIdConnectService
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$idToken->validate($settings->clientId);
|
$idToken->validate($settings->clientId);
|
||||||
} catch (InvalidTokenException $exception) {
|
} catch (OidcInvalidTokenException $exception) {
|
||||||
throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}");
|
throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
|
||||||
|
|
||||||
class InvalidKeyException extends \Exception
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class InvalidTokenException extends Exception
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\OpenIdConnect;
|
|
||||||
|
|
||||||
class IssuerDiscoveryException extends \Exception
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
use BookStack\Auth\Access\OpenIdConnect\OpenIdConnectService;
|
use BookStack\Auth\Access\Oidc\OidcService;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ class OpenIdConnectController extends Controller
|
|||||||
/**
|
/**
|
||||||
* OpenIdController constructor.
|
* OpenIdController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(OpenIdConnectService $oidcService)
|
public function __construct(OidcService $oidcService)
|
||||||
{
|
{
|
||||||
$this->oidcService = $oidcService;
|
$this->oidcService = $oidcService;
|
||||||
$this->middleware('guard:oidc');
|
$this->middleware('guard:oidc');
|
||||||
|
@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
namespace Tests\Unit;
|
namespace Tests\Unit;
|
||||||
|
|
||||||
use BookStack\Auth\Access\OpenIdConnect\InvalidTokenException;
|
use BookStack\Auth\Access\Oidc\OidcInvalidTokenException;
|
||||||
use BookStack\Auth\Access\OpenIdConnect\OpenIdConnectIdToken;
|
use BookStack\Auth\Access\Oidc\OidcIdToken;
|
||||||
use phpseclib3\Crypt\RSA;
|
use phpseclib3\Crypt\RSA;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class OpenIdConnectIdTokenTest extends TestCase
|
class OidcIdTokenTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_valid_token_passes_validation()
|
public function test_valid_token_passes_validation()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [
|
||||||
$this->jwkKeyArray()
|
$this->jwkKeyArray()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -20,20 +20,20 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
|
|
||||||
public function test_get_claim_returns_value_if_existing()
|
public function test_get_claim_returns_value_if_existing()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []);
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []);
|
||||||
$this->assertEquals('bscott@example.com', $token->getClaim('email'));
|
$this->assertEquals('bscott@example.com', $token->getClaim('email'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_get_claim_returns_null_if_not_existing()
|
public function test_get_claim_returns_null_if_not_existing()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []);
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []);
|
||||||
$this->assertEquals(null, $token->getClaim('emails'));
|
$this->assertEquals(null, $token->getClaim('emails'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_get_all_claims_returns_all_payload_claims()
|
public function test_get_all_claims_returns_all_payload_claims()
|
||||||
{
|
{
|
||||||
$defaultPayload = $this->getDefaultPayload();
|
$defaultPayload = $this->getDefaultPayload();
|
||||||
$token = new OpenIdConnectIdToken($this->idToken($defaultPayload), 'https://auth.example.com', []);
|
$token = new OidcIdToken($this->idToken($defaultPayload), 'https://auth.example.com', []);
|
||||||
$this->assertEquals($defaultPayload, $token->getAllClaims());
|
$this->assertEquals($defaultPayload, $token->getAllClaims());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($messagesAndTokenValues as [$message, $tokenValue]) {
|
foreach ($messagesAndTokenValues as [$message, $tokenValue]) {
|
||||||
$token = new OpenIdConnectIdToken($tokenValue, 'https://auth.example.com', []);
|
$token = new OidcIdToken($tokenValue, 'https://auth.example.com', []);
|
||||||
$err = null;
|
$err = null;
|
||||||
try {
|
try {
|
||||||
$token->validate('abc');
|
$token->validate('abc');
|
||||||
@ -60,43 +60,43 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
$err = $exception;
|
$err = $exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertInstanceOf(InvalidTokenException::class, $err, $message);
|
$this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message);
|
||||||
$this->assertEquals($message, $err->getMessage());
|
$this->assertEquals($message, $err->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_error_thrown_if_token_signature_not_validated_from_no_keys()
|
public function test_error_thrown_if_token_signature_not_validated_from_no_keys()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []);
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []);
|
||||||
$this->expectException(InvalidTokenException::class);
|
$this->expectException(OidcInvalidTokenException::class);
|
||||||
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
||||||
$token->validate('abc');
|
$token->validate('abc');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_error_thrown_if_token_signature_not_validated_from_non_matching_key()
|
public function test_error_thrown_if_token_signature_not_validated_from_non_matching_key()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [
|
||||||
array_merge($this->jwkKeyArray(), [
|
array_merge($this->jwkKeyArray(), [
|
||||||
'n' => 'iqK-1QkICMf_cusNLpeNnN-bhT0-9WLBvzgwKLALRbrevhdi5ttrLHIQshaSL0DklzfyG2HWRmAnJ9Q7sweEjuRiiqRcSUZbYu8cIv2hLWYu7K_NH67D2WUjl0EnoHEuiVLsZhQe1CmdyLdx087j5nWkd64K49kXRSdxFQUlj8W3NeK3CjMEUdRQ3H4RZzJ4b7uuMiFA29S2ZhMNG20NPbkUVsFL-jiwTd10KSsPT8yBYipI9O7mWsUWt_8KZs1y_vpM_k3SyYihnWpssdzDm1uOZ8U3mzFr1xsLAO718GNUSXk6npSDzLl59HEqa6zs4O9awO2qnSHvcmyELNk31w'
|
'n' => 'iqK-1QkICMf_cusNLpeNnN-bhT0-9WLBvzgwKLALRbrevhdi5ttrLHIQshaSL0DklzfyG2HWRmAnJ9Q7sweEjuRiiqRcSUZbYu8cIv2hLWYu7K_NH67D2WUjl0EnoHEuiVLsZhQe1CmdyLdx087j5nWkd64K49kXRSdxFQUlj8W3NeK3CjMEUdRQ3H4RZzJ4b7uuMiFA29S2ZhMNG20NPbkUVsFL-jiwTd10KSsPT8yBYipI9O7mWsUWt_8KZs1y_vpM_k3SyYihnWpssdzDm1uOZ8U3mzFr1xsLAO718GNUSXk6npSDzLl59HEqa6zs4O9awO2qnSHvcmyELNk31w'
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
$this->expectException(InvalidTokenException::class);
|
$this->expectException(OidcInvalidTokenException::class);
|
||||||
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
||||||
$token->validate('abc');
|
$token->validate('abc');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_error_thrown_if_token_signature_not_validated_from_invalid_key()
|
public function test_error_thrown_if_token_signature_not_validated_from_invalid_key()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', ['url://example.com']);
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', ['url://example.com']);
|
||||||
$this->expectException(InvalidTokenException::class);
|
$this->expectException(OidcInvalidTokenException::class);
|
||||||
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
$this->expectExceptionMessage('Token signature could not be validated using the provided keys');
|
||||||
$token->validate('abc');
|
$token->validate('abc');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_error_thrown_if_token_algorithm_is_not_rs256()
|
public function test_error_thrown_if_token_algorithm_is_not_rs256()
|
||||||
{
|
{
|
||||||
$token = new OpenIdConnectIdToken($this->idToken([], ['alg' => 'HS256']), 'https://auth.example.com', []);
|
$token = new OidcIdToken($this->idToken([], ['alg' => 'HS256']), 'https://auth.example.com', []);
|
||||||
$this->expectException(InvalidTokenException::class);
|
$this->expectException(OidcInvalidTokenException::class);
|
||||||
$this->expectExceptionMessage("Only RS256 signature validation is supported. Token reports using HS256");
|
$this->expectExceptionMessage("Only RS256 signature validation is supported. Token reports using HS256");
|
||||||
$token->validate('abc');
|
$token->validate('abc');
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($claimOverridesByErrorMessage as [$message, $overrides]) {
|
foreach ($claimOverridesByErrorMessage as [$message, $overrides]) {
|
||||||
$token = new OpenIdConnectIdToken($this->idToken($overrides), 'https://auth.example.com', [
|
$token = new OidcIdToken($this->idToken($overrides), 'https://auth.example.com', [
|
||||||
$this->jwkKeyArray()
|
$this->jwkKeyArray()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
$err = $exception;
|
$err = $exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertInstanceOf(InvalidTokenException::class, $err, $message);
|
$this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message);
|
||||||
$this->assertEquals($message, $err->getMessage());
|
$this->assertEquals($message, $err->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +154,7 @@ class OpenIdConnectIdTokenTest extends TestCase
|
|||||||
$file = tmpfile();
|
$file = tmpfile();
|
||||||
$testFilePath = 'file://' . stream_get_meta_data($file)['uri'];
|
$testFilePath = 'file://' . stream_get_meta_data($file)['uri'];
|
||||||
file_put_contents($testFilePath, $this->pemKey());
|
file_put_contents($testFilePath, $this->pemKey());
|
||||||
$token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [
|
$token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [
|
||||||
$testFilePath
|
$testFilePath
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user