diff --git a/classes/ClientAuthChecker.php b/classes/ClientAuthChecker.php
index cacb8e3ceee6adb1e253cdcaaa899a76958c240f..8bb33e2fdd76623d02559202b51209dab079d099 100644
--- a/classes/ClientAuthChecker.php
+++ b/classes/ClientAuthChecker.php
@@ -17,6 +17,35 @@ class ClientAuthChecker {
 
     public function validateClientAuth(): void {
 
+        $basic = $this->getBasicAuthArray();
+
+        $clientId = $basic[0];
+        $clientSecret = $basic[1];
+
+        $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($clientId);
+        if ($client === null) {
+            throw new UnauthorizedException("Client '$clientId' not configured");
+        }
+        if ($clientSecret !== $client->secret) {
+            throw new UnauthorizedException("Invalid client secret");
+        }
+    }
+
+    public function validateCliClientAuth(): CliClient {
+
+        $basic = $this->getBasicAuthArray();
+
+        $clientId = $basic[0];
+        $clientSecret = $basic[1];
+
+        $client = $this->locator->getOAuth2ClientDAO()->getCliClient($clientId, $clientSecret);
+        if ($client === null) {
+            throw new UnauthorizedException("Client '$clientId' not configured or wrong password");
+        }
+        return $client;
+    }
+
+    private function getBasicAuthArray(): array {
         $headers = apache_request_headers();
 
         if (!isset($headers['Authorization'])) {
@@ -29,16 +58,7 @@ class ClientAuthChecker {
             if (count($basic) !== 2) {
                 throw new BadRequestException("Malformed Basic-Auth header");
             }
-            $clientId = $basic[0];
-            $clientSecret = $basic[1];
-
-            $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($clientId);
-            if ($client === null) {
-                throw new UnauthorizedException("Client '$clientId' not configured");
-            }
-            if ($clientSecret !== $client->secret) {
-                throw new UnauthorizedException("Invalid client secret");
-            }
+            return $basic;
         } else {
             throw new UnauthorizedException("Expected Basic authorization header");
         }
diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php
index 966873fe7cfc8cd27225f19d1218c0cf5c874b2b..d7598164ce1be87705f729caff54f3969bc53675 100644
--- a/classes/JWKSHandler.php
+++ b/classes/JWKSHandler.php
@@ -107,11 +107,7 @@ class JWKSHandler {
                 $dao->updatePublicJWK($jwk);
             }
         } else {
-            $errorMessage = 'Error while retrieving JWKS: ' . curl_error($conn);
-            error_log($result);
-            curl_close($conn);
-            http_response_code(500);
-            die($errorMessage);
+            error_log('Error while retrieving JWKS from ' . $url);
         }
 
         curl_close($conn);
diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php
index 12da4751b44284126ad32fe8364d3072984e844d..8adc55158db553395669c8bbaf91a969cad3c5c4 100644
--- a/classes/OAuth2RequestHandler.php
+++ b/classes/OAuth2RequestHandler.php
@@ -91,7 +91,7 @@ class OAuth2RequestHandler {
         return $redirectUrl;
     }
 
-    public function handleAccessTokenRequest($params): array {
+    public function handleGetTokenFromCodeRequest($params): array {
 
         $this->locator->getClientAuthChecker()->validateClientAuth();
 
@@ -123,6 +123,19 @@ class OAuth2RequestHandler {
         return $response;
     }
 
+    public function handleClientCredentialsRequest($params): array {
+
+        $client = $this->locator->getClientAuthChecker()->validateCliClientAuth();
+
+        $accessTokenData = new AccessTokenData();
+        $accessTokenData->clientId = $client->id;
+        $accessTokenData->userId = $client->id;
+        $accessTokenData->scope = $client->scope;
+        $accessTokenData->audience = $client->audience;
+
+        return $this->getAccessTokenResponse($accessTokenData, false);
+    }
+
     public function handleRefreshTokenRequest($params): array {
 
         $this->locator->getClientAuthChecker()->validateClientAuth();
@@ -182,14 +195,16 @@ class OAuth2RequestHandler {
         return $scope;
     }
 
-    private function getAccessTokenResponse(AccessTokenData $tokenData) {
+    private function getAccessTokenResponse(AccessTokenData $tokenData, bool $refreshToken = true) {
 
         $result = [];
         $result['access_token'] = $this->locator->getTokenBuilder()->getAccessToken($tokenData);
         $result['token_type'] = 'Bearer';
         $result['expires_in'] = $tokenData->expirationTime - time();
 
-        $result['refresh_token'] = $this->buildRefreshToken($tokenData);
+        if ($refreshToken) {
+            $result['refresh_token'] = $this->buildRefreshToken($tokenData);
+        }
 
         if ($tokenData->scope !== null && in_array('openid', $tokenData->scope)) {
             $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData);
diff --git a/classes/TokenBuilder.php b/classes/TokenBuilder.php
index d42103f813787875793ad5bf4e8eac05a2602766..27bf313791e01bbe4e4d2b766e7b4f84c50643b5 100644
--- a/classes/TokenBuilder.php
+++ b/classes/TokenBuilder.php
@@ -58,10 +58,16 @@ class TokenBuilder {
         $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
 
         $user = $this->locator->getUserDAO()->findUserById($tokenData->userId);
+        if ($user === null) {
+            // CLI client
+            $sub = $tokenData->clientId;
+        } else {
+            $sub = $user->id;
+        }
 
         $payload = array(
             'iss' => $this->locator->config->jwtIssuer,
-            'sub' => strval($user->id),
+            'sub' => strval($sub),
             'iat' => intval($tokenData->creationTime),
             'exp' => intval($tokenData->expirationTime),
             'aud' => $this->getAudience($tokenData),
@@ -77,7 +83,15 @@ class TokenBuilder {
 
     private function getAudience(AccessTokenData $tokenData) {
 
+        if ($tokenData->audience !== null) {
+            return $this->getAudienceClaim($tokenData->audience);
+        }
+
         $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($tokenData->clientId);
+        if ($client === null) {
+            // CLI client without audience
+            return null;
+        }
 
         $audiences = [$tokenData->clientId];
 
@@ -90,6 +104,10 @@ class TokenBuilder {
             }
         }
 
+        return $this->getAudienceClaim($audiences);
+    }
+
+    private function getAudienceClaim($audiences) {
         if (count($audiences) === 1) {
             // according to RFC 7519 audience can be a single value or an array
             return $audiences[0];
diff --git a/classes/datalayer/OAuth2ClientDAO.php b/classes/datalayer/OAuth2ClientDAO.php
index ffa4520b21fb64a213166d854de9f104776e4ba1..944ff6e7589e0a9ccb90844e7066a744782f23d0 100644
--- a/classes/datalayer/OAuth2ClientDAO.php
+++ b/classes/datalayer/OAuth2ClientDAO.php
@@ -20,4 +20,6 @@ interface OAuth2ClientDAO {
      * the secret, not the database id).
      */
     function getOAuth2ClientByClientId($clientId): ?OAuth2Client;
+    
+    function getCliClient(string $clientId, string $secret): ?CliClient; 
 }
diff --git a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
index 7890e95c8ee1b5b48fa4814a1f03a81d81c7070a..c005b692b15461025ed51790ce3e756c130222d8 100644
--- a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
+++ b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
@@ -246,4 +246,41 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
         return $client;
     }
 
+    function getCliClient(string $clientId, string $secret): ?CliClient {
+
+        $dbh = $this->getDBHandler();
+
+        // Load clients info
+        $queryClient = "SELECT scope, audience FROM cli_client WHERE client_id = :client AND client_secret = PASSWORD(:secret)";
+        $stmtClient = $dbh->prepare($queryClient);
+        $stmtClient->bindParam(':client', $clientId);
+        $stmtClient->bindParam(':secret', $secret);
+        $stmtClient->execute();
+
+        $result = $stmtClient->fetchAll();
+
+        if (count($result) === 0) {
+            return null;
+        }
+        if (count($result) > 1) {
+            throw new \Exception("Found multiple clients associated to the same client id!");
+        }
+
+        $row = $result[0];
+
+        $client = new CliClient();
+        $client->id = $clientId;
+        if ($row['scope'] !== null) {
+            $client->scope = explode(' ', $row['scope']);
+        } else {
+            $client->scope = [];
+        }
+        if ($row['audience'] !== null) {
+            $client->audience = explode(' ', $row['audience']);
+        } else {
+            $client->audience = [];
+        }
+        return $client;
+    }
+
 }
diff --git a/classes/model/AccessTokenData.php b/classes/model/AccessTokenData.php
index 3826e22a14a040dceb6464a8b2969f58c891e9e8..a73d06b359a4df37ec0ac8aad93c3b5b35a64943 100644
--- a/classes/model/AccessTokenData.php
+++ b/classes/model/AccessTokenData.php
@@ -19,6 +19,7 @@ class AccessTokenData {
     public $redirectUri;
     public $clientId;
     public $scope;
+    public $audience;
 
     public function __construct() {
         $this->creationTime = time();
diff --git a/classes/model/CliClient.php b/classes/model/CliClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..d10f719177f276a71e4cc62a7dcd41ced7ecca32
--- /dev/null
+++ b/classes/model/CliClient.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace RAP;
+
+class CliClient {
+
+    public $id;
+    public $scope;
+    public $audience;
+
+}
diff --git a/include/front-controller.php b/include/front-controller.php
index 7f5714b068e82e90757494ae90f34ab3c872e29c..540a16d929acb033c2ab0fb829c5e3870bc606f3 100644
--- a/include/front-controller.php
+++ b/include/front-controller.php
@@ -106,7 +106,10 @@ Flight::route('POST /auth/oauth2/token', function() {
 
     switch ($params['grant_type']) {
         case "authorization_code":
-            $token = $requestHandler->handleAccessTokenRequest($params);
+            $token = $requestHandler->handleGetTokenFromCodeRequest($params);
+            break;
+        case "client_credentials":
+            $token = $requestHandler->handleClientCredentialsRequest($params);
             break;
         case "refresh_token":
             $token = $requestHandler->handleRefreshTokenRequest($params);
diff --git a/sql/setup-database.sql b/sql/setup-database.sql
index 5b3842b0e1b0db1004bbb5b76e743c1eb605e5f8..6e8b7f6e3b5e9ddb6a6b021114f7791c52aad3d3 100644
--- a/sql/setup-database.sql
+++ b/sql/setup-database.sql
@@ -26,6 +26,14 @@ CREATE TABLE `oauth2_client_scope_audience_mapping` (
   FOREIGN KEY (`client_id`) REFERENCES `oauth2_client`(`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+CREATE TABLE `cli_client` (
+  `client_id` varchar(255) NOT NULL,
+  `client_secret` varchar(255) NOT NULL,
+  `scope` text,
+  `audience` text,
+  PRIMARY KEY (`client_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
 CREATE TABLE `user` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
   `primary_identity` bigint(20) DEFAULT NULL,