diff options
| author | Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> | 2022-07-15 11:47:35 +0000 |
|---|---|---|
| committer | Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> | 2022-07-15 11:47:35 +0000 |
| commit | 55852ef4819e5eafce9ae53dc4de2d84cdad1778 (patch) | |
| tree | 9aedcdf89f416a7936f7df80da339a537082b5d5 /lib/classes/OAuth2/Bridge | |
| parent | a9585dad3547a4ebbadd00f44065f95017d18684 (diff) | |
StEP-366: Add OAuth2 support to Stud.IP
Closes #1035 and #1198
Merge request studip/studip!635
Diffstat (limited to 'lib/classes/OAuth2/Bridge')
| -rw-r--r-- | lib/classes/OAuth2/Bridge/AccessTokenEntity.php | 34 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/AccessTokenRepository.php | 74 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/AuthCodeEntity.php | 15 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/AuthCodeRepository.php | 69 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/ClientEntity.php | 27 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/ClientRepository.php | 59 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/RefreshTokenEntity.php | 13 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/RefreshTokenRepository.php | 63 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/ScopeEntity.php | 18 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/ScopeRepository.php | 60 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/ScopesHelper.php | 18 | ||||
| -rw-r--r-- | lib/classes/OAuth2/Bridge/UserEntity.php | 16 |
12 files changed, 466 insertions, 0 deletions
diff --git a/lib/classes/OAuth2/Bridge/AccessTokenEntity.php b/lib/classes/OAuth2/Bridge/AccessTokenEntity.php new file mode 100644 index 0000000..987f0a0 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/AccessTokenEntity.php @@ -0,0 +1,34 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\Traits\AccessTokenTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; + +class AccessTokenEntity implements AccessTokenEntityInterface +{ + use AccessTokenTrait; + use EntityTrait; + use TokenEntityTrait; + + /** + * Create a new token instance. + * + * @param string $userIdentifier + * + * @return void + */ + public function __construct($userIdentifier, array $scopes, ClientEntityInterface $client) + { + $this->setUserIdentifier($userIdentifier); + + foreach ($scopes as $scope) { + $this->addScope($scope); + } + + $this->setClient($client); + } +} diff --git a/lib/classes/OAuth2/Bridge/AccessTokenRepository.php b/lib/classes/OAuth2/Bridge/AccessTokenRepository.php new file mode 100644 index 0000000..2762f6b --- /dev/null +++ b/lib/classes/OAuth2/Bridge/AccessTokenRepository.php @@ -0,0 +1,74 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use Studip\OAuth2\Models\AccessToken; + +class AccessTokenRepository implements AccessTokenRepositoryInterface +{ + use ScopesHelper; + + /** + * Create a new access token. + * + * @param ScopeEntityInterface[] $scopes + * @param mixed $userIdentifier + * + * @return AccessTokenEntityInterface + */ + public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) + { + return new AccessTokenEntity($userIdentifier, $scopes, $clientEntity); + } + + /** + * Persists a new access token to permanent storage. + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity): void + { + AccessToken::create([ + 'id' => $accessTokenEntity->getIdentifier(), + 'user_id' => $accessTokenEntity->getUserIdentifier(), + 'client_id' => $accessTokenEntity->getClient()->getIdentifier(), + 'scopes' => $this->formatScopes($accessTokenEntity->getScopes()), + 'revoked' => 0, + 'expires_at' => $accessTokenEntity->getExpiryDateTime()->getTimestamp(), + ]); + + // TODO: Logging and metrics + } + + /** + * Revoke an access token. + * + * @param string $tokenId + */ + public function revokeAccessToken($tokenId): void + { + $accesstoken = AccessToken::find($tokenId); + if ($accesstoken) { + $accesstoken->revoke(); + } + } + + /** + * Check if the access token has been revoked. + * + * @param string $tokenId + * + * @return bool Return true if this token has been revoked + */ + public function isAccessTokenRevoked($tokenId): bool + { + $accesstoken = AccessToken::find($tokenId); + + return $accesstoken ? $accesstoken->isRevoked() : true; + } +} diff --git a/lib/classes/OAuth2/Bridge/AuthCodeEntity.php b/lib/classes/OAuth2/Bridge/AuthCodeEntity.php new file mode 100644 index 0000000..5514968 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/AuthCodeEntity.php @@ -0,0 +1,15 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\AuthCodeEntityInterface; +use League\OAuth2\Server\Entities\Traits\AuthCodeTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; + +class AuthCodeEntity implements AuthCodeEntityInterface +{ + use EntityTrait; + use TokenEntityTrait; + use AuthCodeTrait; +} diff --git a/lib/classes/OAuth2/Bridge/AuthCodeRepository.php b/lib/classes/OAuth2/Bridge/AuthCodeRepository.php new file mode 100644 index 0000000..5676622 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/AuthCodeRepository.php @@ -0,0 +1,69 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\AuthCodeEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; +use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; +use Studip\OAuth2\Models\AuthCode; + +class AuthCodeRepository implements AuthCodeRepositoryInterface +{ + use ScopesHelper; + + /** + * Creates a new AuthCode. + */ + public function getNewAuthCode(): AuthCodeEntityInterface + { + return new AuthCodeEntity(); + } + + /** + * Persists a new auth code to permanent storage. + * + * @return void + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity) + { + AuthCode::create([ + 'id' => $authCodeEntity->getIdentifier(), + 'user_id' => $authCodeEntity->getUserIdentifier(), + 'client_id' => $authCodeEntity->getClient()->getIdentifier(), + 'scopes' => $this->formatScopes($authCodeEntity->getScopes()), + 'revoked' => 0, + 'expires_at' => $authCodeEntity->getExpiryDateTime()->getTimestamp(), + ]); + + // TODO: Logging and metrics + } + + /** + * Revoke an auth code. + * + * @param string $codeId + */ + public function revokeAuthCode($codeId): void + { + $authCode = AuthCode::find($codeId); + if ($authCode) { + $authCode->revoke(); + } + } + + /** + * Check if the auth code has been revoked. + * + * @param string $codeId + * + * @return bool Return true if this code has been revoked + */ + public function isAuthCodeRevoked($codeId): bool + { + $authCode = AuthCode::find($codeId); + + return $authCode ? $authCode->isRevoked() : true; + } +} diff --git a/lib/classes/OAuth2/Bridge/ClientEntity.php b/lib/classes/OAuth2/Bridge/ClientEntity.php new file mode 100644 index 0000000..106caa0 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/ClientEntity.php @@ -0,0 +1,27 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\Traits\ClientTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; + +class ClientEntity implements ClientEntityInterface +{ + use ClientTrait; + use EntityTrait; + + /** + * @param string $identifier + * @param string $name + * @param string|string[] $redirectUri + * @param bool $isConfidential + */ + public function __construct($identifier, $name, $redirectUri, $isConfidential) + { + $this->identifier = $identifier; + $this->name = $name; + $this->redirectUri = $redirectUri; + $this->isConfidential = $isConfidential; + } +} diff --git a/lib/classes/OAuth2/Bridge/ClientRepository.php b/lib/classes/OAuth2/Bridge/ClientRepository.php new file mode 100644 index 0000000..b6fd4f6 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/ClientRepository.php @@ -0,0 +1,59 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Repositories\ClientRepositoryInterface; +use Studip\OAuth2\Models\Client; + +class ClientRepository implements ClientRepositoryInterface +{ + /** + * Get a client. + * + * @param string $clientIdentifier The client's identifier + */ + public function getClientEntity($clientIdentifier): ?ClientEntityInterface + { + $sorm = Client::findActive($clientIdentifier); + if (!$sorm) { + return null; + } + + return new ClientEntity( + $clientIdentifier, + $sorm['name'], + explode(',', $sorm['redirect']), + $sorm->confidential() + ); + } + + /** + * Validate a client's secret. + * + * @param string $clientIdentifier The client's identifier + * @param string|null $clientSecret The client's secret (if sent) + * @param string|null $grantType The type of grant the client is using (if sent) + */ + public function validateClient($clientIdentifier, $clientSecret, $grantType): bool + { + if ($grantType !== 'authorization_code') { + return false; + } + $client = Client::findActive($clientIdentifier); + if (!$client) { + return false; + } + + return !$client->confidential() || $this->verifySecret((string) $clientSecret, $client->secret); + } + + /** + * @param string $clientSecret + * @param string $storedHash + */ + protected function verifySecret($clientSecret, $storedHash): bool + { + return password_verify($clientSecret, $storedHash); + } +} diff --git a/lib/classes/OAuth2/Bridge/RefreshTokenEntity.php b/lib/classes/OAuth2/Bridge/RefreshTokenEntity.php new file mode 100644 index 0000000..a0dda5e --- /dev/null +++ b/lib/classes/OAuth2/Bridge/RefreshTokenEntity.php @@ -0,0 +1,13 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait; + +class RefreshTokenEntity implements RefreshTokenEntityInterface +{ + use RefreshTokenTrait; + use EntityTrait; +} diff --git a/lib/classes/OAuth2/Bridge/RefreshTokenRepository.php b/lib/classes/OAuth2/Bridge/RefreshTokenRepository.php new file mode 100644 index 0000000..44cb16c --- /dev/null +++ b/lib/classes/OAuth2/Bridge/RefreshTokenRepository.php @@ -0,0 +1,63 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use Studip\OAuth2\Models\RefreshToken; + +class RefreshTokenRepository implements RefreshTokenRepositoryInterface +{ + /** + * Creates a new refresh token. + */ + public function getNewRefreshToken(): RefreshTokenEntityInterface + { + return new RefreshTokenEntity(); + } + + /** + * Create a new refresh token_name. + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void + { + RefreshToken::create([ + 'id' => $refreshTokenEntity->getIdentifier(), + 'access_token_id' => $refreshTokenEntity->getAccessToken()->getIdentifier(), + 'revoked' => 0, + 'expires_at' => $refreshTokenEntity->getExpiryDateTime()->getTimestamp(), + ]); + + // TODO: Logging and metrics + } + + /** + * Revoke the refresh token. + * + * @param string $tokenId + */ + public function revokeRefreshToken($tokenId): void + { + $refreshToken = RefreshToken::find($tokenId); + if ($refreshToken) { + $refreshToken->revoke(); + } + } + + /** + * Check if the refresh token has been revoked. + * + * @param string $tokenId + * + * @return bool Return true if this token has been revoked + */ + public function isRefreshTokenRevoked($tokenId): bool + { + $refreshToken = RefreshToken::find($tokenId); + + return $refreshToken ? $refreshToken->isRevoked() : true; + } +} diff --git a/lib/classes/OAuth2/Bridge/ScopeEntity.php b/lib/classes/OAuth2/Bridge/ScopeEntity.php new file mode 100644 index 0000000..844600a --- /dev/null +++ b/lib/classes/OAuth2/Bridge/ScopeEntity.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\ScopeTrait; + +class ScopeEntity implements ScopeEntityInterface +{ + use ScopeTrait; + use EntityTrait; + + public function __construct(string $identifier) + { + $this->setIdentifier($identifier); + } +} diff --git a/lib/classes/OAuth2/Bridge/ScopeRepository.php b/lib/classes/OAuth2/Bridge/ScopeRepository.php new file mode 100644 index 0000000..65d666e --- /dev/null +++ b/lib/classes/OAuth2/Bridge/ScopeRepository.php @@ -0,0 +1,60 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use Psr\Container\ContainerInterface; +use Studip\OAuth2\Models\Scope; + +class ScopeRepository implements ScopeRepositoryInterface +{ + /** @var array<string, string> */ + private $scopes; + + public function __construct(ContainerInterface $container) + { + $this->scopes = Scope::scopes(); + } + + /** + * Return information about a scope. + * + * @param string $identifier The scope identifier + */ + public function getScopeEntityByIdentifier($identifier): ?ScopeEntityInterface + { + if (!isset($this->scopes[$identifier])) { + return null; + } + + return new ScopeEntity($identifier); + } + + /** + * Given a client, grant type and optional user identifier validate + * the set of scopes requested are valid and + * optionally append additional scopes or remove requested scopes. + * + * @param ScopeEntityInterface[] $scopes + * @param string $grantType + * @param ClientEntityInterface $clientEntity + * @param null|string $userIdentifier + * + * @return ScopeEntityInterface[] + */ + public function finalizeScopes( + array $scopes, + $grantType, + ClientEntityInterface $clientEntity, + $userIdentifier = null + ) { + return array_filter( + $scopes, + function ($scope) { + return isset($this->scopes[$scope->getIdentifier()]); + } + ); + } +} diff --git a/lib/classes/OAuth2/Bridge/ScopesHelper.php b/lib/classes/OAuth2/Bridge/ScopesHelper.php new file mode 100644 index 0000000..d075381 --- /dev/null +++ b/lib/classes/OAuth2/Bridge/ScopesHelper.php @@ -0,0 +1,18 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +trait ScopesHelper +{ + public function formatScopes(array $scopes): string + { + return json_encode($this->scopesToArray($scopes)); + } + + public function scopesToArray(array $scopes): array + { + return array_map(function ($scope) { + return $scope->getIdentifier(); + }, $scopes); + } +} diff --git a/lib/classes/OAuth2/Bridge/UserEntity.php b/lib/classes/OAuth2/Bridge/UserEntity.php new file mode 100644 index 0000000..02ba52f --- /dev/null +++ b/lib/classes/OAuth2/Bridge/UserEntity.php @@ -0,0 +1,16 @@ +<?php + +namespace Studip\OAuth2\Bridge; + +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\UserEntityInterface; + +class UserEntity implements UserEntityInterface +{ + use EntityTrait; + + public function __construct(string $identifier) + { + $this->setIdentifier($identifier); + } +} |
