diff --git a/migrations/Version20230730131708.php b/migrations/Version20230730131708.php new file mode 100644 index 00000000..2d5c0024 --- /dev/null +++ b/migrations/Version20230730131708.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE oauth_tokens CHANGE token token LONGTEXT DEFAULT NULL, CHANGE refresh_token refresh_token LONGTEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE oauth_tokens CHANGE token token VARCHAR(255) DEFAULT NULL, CHANGE refresh_token refresh_token VARCHAR(255) NOT NULL'); + } +} diff --git a/src/Entity/OAuthToken.php b/src/Entity/OAuthToken.php index 30a8feef..b1534a4d 100644 --- a/src/Entity/OAuthToken.php +++ b/src/Entity/OAuthToken.php @@ -39,26 +39,34 @@ use League\OAuth2\Client\Token\AccessTokenInterface; class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface { /** @var string|null The short-term usable OAuth2 token */ - #[ORM\Column(type: 'string', nullable: true)] + #[ORM\Column(type: 'text', nullable: true)] private ?string $token = null; /** @var \DateTimeInterface The date when the token expires */ #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] private ?\DateTimeInterface $expires_at = null; - /** @var string The refresh token for the OAuth2 auth */ - #[ORM\Column(type: 'string')] - private string $refresh_token = ''; + /** @var string|null The refresh token for the OAuth2 auth */ + #[ORM\Column(type: 'text', nullable: true)] + private ?string $refresh_token = null; + /** + * The default expiration time for a authorization token, if no expiration time is given + */ private const DEFAULT_EXPIRATION_TIME = 3600; - public function __construct(string $name, string $refresh_token, string $token = null, \DateTimeInterface $expires_at = null) + public function __construct(string $name, ?string $refresh_token, ?string $token = null, \DateTimeInterface $expires_at = null) { //If token is given, you also have to give the expires_at date if ($token !== null && $expires_at === null) { throw new \InvalidArgumentException('If you give a token, you also have to give the expires_at date'); } + //If no refresh_token is given, the token is a client credentials grant token, which must have a token + if ($refresh_token === null && $token === null) { + throw new \InvalidArgumentException('If you give no refresh_token, you have to give a token!'); + } + $this->name = $name; $this->refresh_token = $refresh_token; $this->expires_at = $expires_at; @@ -109,6 +117,16 @@ class OAuthToken extends AbstractNamedDBElement implements AccessTokenInterface return $this->expires_at->getTimestamp() < time(); } + /** + * Returns true if this token is a client credentials grant token (meaning it has no refresh token), and + * needs to be refreshed via the client credentials grant. + * @return bool + */ + public function isClientCredentialsGrant(): bool + { + return $this->refresh_token === null; + } + public function replaceWithNewToken(AccessTokenInterface $accessToken): void { $this->token = $accessToken->getToken(); diff --git a/src/Services/OAuth/OAuthTokenManager.php b/src/Services/OAuth/OAuthTokenManager.php index 020eead7..1f6f6b51 100644 --- a/src/Services/OAuth/OAuthTokenManager.php +++ b/src/Services/OAuth/OAuthTokenManager.php @@ -39,9 +39,9 @@ final class OAuthTokenManager * Saves the given token to the database, so it can be retrieved later * @param string $app_name * @param AccessTokenInterface $token - * @return void + * @return OAuthToken The saved token as database entity */ - public function saveToken(string $app_name, AccessTokenInterface $token): void + public function saveToken(string $app_name, AccessTokenInterface $token): OAuthToken { //Check if we already have a token for this app $tokenEntity = $this->entityManager->getRepository(OAuthToken::class)->findOneBy(['name' => $app_name]); @@ -54,7 +54,7 @@ final class OAuthTokenManager $this->entityManager->flush($tokenEntity); //We are done - return; + return $tokenEntity; } //If the token was not existing, we create a new one @@ -63,7 +63,7 @@ final class OAuthTokenManager //@phpstan-ignore-next-line $this->entityManager->flush($tokenEntity); - return; + return $tokenEntity; } /** @@ -102,7 +102,14 @@ final class OAuthTokenManager } $client = $this->clientRegistry->getClient($app_name); - $new_token = $client->refreshAccessToken($token->getRefreshToken()); + + //Check if the token is refreshable or if it is an client credentials token + if ($token->isClientCredentialsGrant()) { + $new_token = $client->getOAuth2Provider()->getAccessToken('client_credentials'); + } else { + //Otherwise we can use the refresh token to get a new access token + $new_token = $client->refreshAccessToken($token->getRefreshToken()); + } //Persist the token $token->replaceWithNewToken($new_token); @@ -139,4 +146,20 @@ final class OAuthTokenManager //And return the new token return $token->getToken(); } + + /** + * Retrieves an access token for the given app name using the client credentials grant (so no user flow is needed) + * The app_name must be registered in the knpu_oauth2_client.yaml + * The token is saved to the database, and afterward can be used as usual + * @param string $app_name + * @return OAuthToken + */ + public function retrieveClientCredentialsToken(string $app_name): OAuthToken + { + $client = $this->clientRegistry->getClient($app_name); + $access_token = $client->getOAuth2Provider()->getAccessToken('client_credentials'); + + + return $this->saveToken($app_name, $access_token); + } } \ No newline at end of file