Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ Install the add-on with Composer:
composer require iansimpson/ss-oauth2-server
```

Next, generate a private/public key pair:

```sh
openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout -out public.key
Next, generate a private/public key pair using Ed25519 algorithm:
```shell
openssl genpkey -algorithm Ed25519 -out private.key
openssl pkey -in private.key -pubout -out public.key
chmod 600 private.key
chmod 600 public.key
```
Expand Down
134 changes: 134 additions & 0 deletions code/AuthorizationValidators/BearerTokenValidatorEddsa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace IanSimpson\OAuth2\AuthorizationValidators;

use DateTimeZone;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Eddsa;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use IanSimpson\OAuth2\Utility\Utility;

/**
* Majority of logic was taken from {@link BearerTokenValidator} due to `$jwtConfiguration` property set to private
* without any accessor methods available.
*
* An alternative would be to implement `AuthorizationValidatorInterface` but since most of the properties are almost
* similar with `BearerTokenValidator`, extending the class makes sense.
*/
class BearerTokenValidatorEddsa extends BearerTokenValidator
{

/**
* @var Configuration
*/
private $jwtConfiguration;

/**
* @inheritDoc
*/
public function __construct(AccessTokenRepositoryInterface $accessTokenRepository, \DateInterval $jwtValidAtDateLeeway = null)
{
$this->accessTokenRepository = $accessTokenRepository;
$this->jwtValidAtDateLeeway = $jwtValidAtDateLeeway;
}

/**
* @inheritDoc
*/
public function setPublicKey(CryptKey $key)
{
$this->publicKey = $key;

$this->initJwtConfiguration();
}

/**
* @inheritDoc
*/
public function validateAuthorization(ServerRequestInterface $request)
{
if ($request->hasHeader('authorization') === false) {
throw OAuthServerException::accessDenied('Missing "Authorization" header');
}

$header = $request->getHeader('authorization');
$jwt = \trim((string) \preg_replace('/^\s*Bearer\s/', '', $header[0]));

try {
// Attempt to parse the JWT
$token = $this->jwtConfiguration->parser()->parse($jwt);
} catch (\Lcobucci\JWT\Exception $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
}

try {
// Attempt to validate the JWT
$constraints = $this->jwtConfiguration->validationConstraints();
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception);
}

$claims = $token->claims();

// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}

// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $claims->get('jti'))
->withAttribute('oauth_client_id', $this->convertSingleRecordAudToString($claims->get('aud')))
->withAttribute('oauth_user_id', $claims->get('sub'))
->withAttribute('oauth_scopes', $claims->get('scopes'));
}

/**
* Override this to use a different signer compatible with EdDSA algorithm
*/
protected function initJwtConfiguration()
{
$this->jwtConfiguration = Configuration::forSymmetricSigner(
new Eddsa(),
InMemory::plainText('empty', 'empty')
);

$clock = new SystemClock(new DateTimeZone(\date_default_timezone_get()));

// Extract PEM formatted key from generated public key
$publicKeyResource = $this->publicKey->getKeyContents();

// Extract the public key in DER format
$derPublicKey = Utility::extractDERKeyValue($publicKeyResource);

$this->jwtConfiguration->setValidationConstraints(
new LooseValidAt($clock, $this->jwtValidAtDateLeeway),
new SignedWith(
new Eddsa(),
InMemory::plainText($derPublicKey, $this->publicKey->getPassPhrase() ?? '')
)
);
}

/**
* Convert single record arrays into strings to ensure backwards compatibility between v4 and v3.x of lcobucci/jwt
*
* @inheritDoc
*/
private function convertSingleRecordAudToString($aud)
{
return \is_array($aud) && \count($aud) === 1 ? $aud[0] : $aud;
}
}
42 changes: 42 additions & 0 deletions code/Entities/AccessTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
use DateTimeImmutable;
use Exception;
use IanSimpson\OAuth2\OauthServerController;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Eddsa;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
Expand All @@ -20,6 +24,9 @@
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\Security\Member;
use Lcobucci\JWT\Builder;
use League\OAuth2\Server\CryptKey;
use IanSimpson\OAuth2\Utility\Utility;

/**
* @property ?string $Code
Expand Down Expand Up @@ -82,6 +89,41 @@ class AccessTokenEntity extends DataObject implements AccessTokenEntityInterface
'Code',
];

/**
* Generate a JWT from the access token
*
* @return Token
*/
public function convertToJWT()
{
// Extract the PEM key generated
$pemPrivateKey = $this->privateKey->getKeyContents();

// Extract the DER formatted key to use as seed for generating the private/secret key
$derKey = Utility::extractDERKeyValue($pemPrivateKey);

// Generate the secret key via Sodium library
$secretkey = sodium_crypto_sign_secretkey(sodium_crypto_sign_seed_keypair($derKey));

// Configure the JWT generation
$config = Configuration::forAsymmetricSigner(
new Eddsa(),
InMemory::plainText($secretkey),
InMemory::plainText('empty', 'empty')
);

// return the token
return $config->builder()
->permittedFor($this->getClient()->getIdentifier())
->identifiedBy($this->getIdentifier())
->issuedAt(new DateTimeImmutable())
->canOnlyBeUsedAfter(new DateTimeImmutable())
->expiresAt($this->getExpiryDateTime())
->relatedTo((string) $this->getUserIdentifier())
->withClaim('scopes', $this->getScopes())
->getToken($config->signer(), $config->signingKey());
}

public function getIdentifier(): string
{
return (string) $this->Code;
Expand Down
7 changes: 5 additions & 2 deletions code/OauthServerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use IanSimpson\OAuth2\AuthorizationValidators\BearerTokenValidatorEddsa;

class OauthServerController extends Controller
{
Expand Down Expand Up @@ -290,7 +291,8 @@ public static function authenticateRequest($controller): ?ServerRequestInterface

$server = new ResourceServer(
new AccessTokenRepository(),
$publicKey
$publicKey,
new BearerTokenValidatorEddsa(new AccessTokenRepository())
);

$request = ServerRequest::fromGlobals();
Expand Down Expand Up @@ -331,7 +333,8 @@ public function validateClientGrant(HTTPRequest $request): HTTPResponse
{
$server = new ResourceServer(
new AccessTokenRepository(),
$this->publicKey
$this->publicKey,
new BearerTokenValidatorEddsa(new AccessTokenRepository())
);

$this->handleRequest($request);
Expand Down
32 changes: 32 additions & 0 deletions code/Utility/Utility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace IanSimpson\OAuth2\Utility;

/**
* Contains helper functions that can be used across
*/
class Utility
{

/**
* Converts the given PEM key generated from `openssl` to DER format and extracts the last 32 bytes.
* If a private key (private.key) is provided, the last 32 bytes represent the private key which can be used
* for generating Sodium-compatible key format.
*
* If a public key (public.key) is provided, the last 32 bytes represent the Sodium-compatible public key that can
* be used for validation.
*
* @param string $pemKey
* @return string
*/
public static function extractDERKeyValue(string $pemKey): string
{
if (empty($pemKey)) {
return '';
}

$derKey = base64_decode(preg_replace('/-+.*?-+|\s/', '', $pemKey));

return substr($derKey, -32);
}
}
6 changes: 3 additions & 3 deletions tests/OauthServerControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use IanSimpson\OAuth2\OauthServerController;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Signer\Eddsa;
use Lcobucci\JWT\Validation\Constraint\IdentifiedBy;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\PermittedFor;
Expand Down Expand Up @@ -212,7 +212,7 @@ public function testAccessTokenUserID(): void
$this->assertNotEmpty($m->ID);

$configuration = Configuration::forSymmetricSigner(
new Sha256(),
new Eddsa(),
InMemory::file($this->privateKey)
);

Expand Down Expand Up @@ -287,7 +287,7 @@ public function testAccessTokenClientCredentials(): void
$this->assertNotEmpty($c->ClientIdentifier);

$configuration = Configuration::forSymmetricSigner(
new Sha256(),
new Eddsa(),
InMemory::file($this->privateKey)
);

Expand Down
30 changes: 3 additions & 27 deletions tests/private.key
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvV8XeuqgS1J26dZDxpWBHowRUagkvoKb96zggk91MIK2Xat1
zmV3OcdnsEDxkH1IVIAbiRSlvmet+qMhjHy7md3c1Wnbu7Pjgi/sOnd8zr9Ole1s
IUXLoPYps4xEBbF1sihXQ+zIk4vEaPe6DfQpHqaduDoIrw1VjrZ90p3iFXwhVrvW
keBQRAPF3NyHiLLBaSzFPl+m8GYSs/bBUAHucT1TDAr0XpmAoxHbM60OxcqETVRF
gnUaY1YhH0L1gEB5lgYtPQgGkJHiLmACSxzK7a7q61BPbtgMpb6Uee0CJ9ohJq+R
XC0DcS6ICfhxwqtyowzJEWAjrZbnFbvnAMSBkwIDAQABAoIBAACxiHR6x3t/IdBZ
aIYhpgdmm+mgpAHOmKOfjnLrt5Il6GUPa68F0Bn2EPskQb41tz4X+gHWgYTg/FIe
ptDM5CL8HQLrEFLvpbWkV0kfhuV67d6+r9avWr+MJzrxGI6sx7GPVEJ7a4Ce0mlP
/u5uJnhmQ82Y6M87TUTohi1vRpStjhKClcvgXZzV9lIqHlE/oOLuHwtBbnN3FeE6
8B8uxLrbzCd72uIc+BU+1R5/qj7KKUoaiKdbdzrIT17MEow7uOClUUKPTspYsfLY
UcYNdcwh73XkWgIYthQcgcJB/ThIGED+ghHGILkywJctiryMEBd5LA62mlqnNw97
Nb0h+wECgYEA9PoUkml1bzZJvInCAhCPBCkxEyTvXQIJEssfSDrCgXuiW2OGyIe+
LJXYTrvB0dH7+AapjJBUtG2WE3hP3Ae+LQfGRMbUT2fHNo3McCa+gT1XkE8t7guJ
On21EILEU2ycwFS1inHrZI7iPcN/8QYyorVsjz2cd0v0vgWFaCOlr8kCgYEAxeR8
RcyC5Anp3oXVvZx2y5Zf/W5DzT3Kf5Ki1LAIDVtxWo7oC9+ML45hsXLQ2DNfh7oO
LmUy+OUssLzgmSUFYX8bve0QJFQB0oSOpPH8UIX1p5gRa5ObbyVHhUh+krWSI+zl
sUfpn5pS6lmHhwqNdtKthr/cIoIXzJZTD5CerHsCgYEAp+CEnnz0g/zQ9Qn2UJHm
X/SWc8cY3UTa0O0vh8D0r+T4suX2ZI9ZTJZ3QCU8wjvDDwoJwJDb6zU55ifJmkAY
HCW6wkD7F1tH0mPD2ItEe658xiDsmbeAF/wzS5hBT+YbWVXzfmdo52VfNvI1SAd2
RbhMJ7ER+1Kq6llAPchH77kCgYAhXnziHDE6GL3Z6wF2vqp9e/blujEuq6u4xVY1
vRUug2vi1FQmpGR0JHMuw+iZfFgwf9wiUKg+tg5KIx9QU6DLpu5boVzmc0/3Wqjf
AXsFbQFWaFsPo81C+atMu0O0o29oJWs58KFha1lt8PjceZgPIElofnO1UfCHbBXH
eyB2fwKBgErOGtpA2kVTp5hThZSsiIc33LQqW6aI8JUSGiS3kReVHyoc88nn3n0Q
sNAKM6vDMqwGNmm6J9kTs2zrXXNvHOT+KxwL7/gWqXBvKm0defVGUywuSOxBTtSK
lJCkzL3aNfyoQfSAtavF43UzakTH3YViqV62tDeKPY/1HieeLm/1
-----END RSA PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIO8ZlgnqG8D2Jnef4RhvTLeSYGN9FDXz1sl6cui+JYJT
-----END PRIVATE KEY-----
8 changes: 1 addition & 7 deletions tests/public.key
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvV8XeuqgS1J26dZDxpWB
HowRUagkvoKb96zggk91MIK2Xat1zmV3OcdnsEDxkH1IVIAbiRSlvmet+qMhjHy7
md3c1Wnbu7Pjgi/sOnd8zr9Ole1sIUXLoPYps4xEBbF1sihXQ+zIk4vEaPe6DfQp
HqaduDoIrw1VjrZ90p3iFXwhVrvWkeBQRAPF3NyHiLLBaSzFPl+m8GYSs/bBUAHu
cT1TDAr0XpmAoxHbM60OxcqETVRFgnUaY1YhH0L1gEB5lgYtPQgGkJHiLmACSxzK
7a7q61BPbtgMpb6Uee0CJ9ohJq+RXC0DcS6ICfhxwqtyowzJEWAjrZbnFbvnAMSB
kwIDAQAB
MCowBQYDK2VwAyEAnMkew9Bl7oMb8191TUgu3L7CGSN8VmzUOc/6u03dtA0=
-----END PUBLIC KEY-----