Repository: gravitee-io/graviteeio-access-management
Pull request #550 (2019-03-28 13:41:19)

feat(client): renew OAuth 2.0 client secret
closes gravitee-io/issues#2063


Patch:
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
index 697e68e6e2..764839edff 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/main/java/io/gravitee/am/management/handlers/management/api/resources/ClientResource.java
@@ -141,6 +141,25 @@ public class ClientResource extends AbstractResource {
                         error -> response.resume(error));
     }
 
+    @POST
+    @Path("secret/_renew")
+    @ApiOperation(value = "Renew client secret")
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "Client secret successfully updated", response = Client.class),
+            @ApiResponse(code = 500, message = "Internal server error")})
+    public void renewClientSecret(@PathParam("domain") String domain,
+                            @PathParam("client") String client,
+                            @Suspended final AsyncResponse response) {
+        domainService.findById(domain)
+                .switchIfEmpty(Maybe.error(new DomainNotFoundException(domain)))
+                .flatMapSingle(__ -> clientService.renewClientSecret(domain, client))
+                .map(updatedClient -> Response.ok(updatedClient).build())
+                .subscribe(
+                        result -> response.resume(result),
+                        error -> response.resume(error));
+    }
+
     @Path("emails")
     public ClientEmailsResource getEmailsResource() {
         return resourceContext.getResource(ClientEmailsResource.class);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
index be3a48dc51..b3f2cf73f5 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/ClientService.java
@@ -32,53 +32,55 @@ import java.util.Set;
  */
 public interface ClientService {
 
-    Maybe<Client> findById(String id);
-
-    Maybe<Client> findByDomainAndClientId(String domain, String clientId);
+    Single<Set<Client>> findAll();
 
-    Single<Page<Client>> findByDomain(String domain, int page, int size);
+    Single<Page<Client>> findAll(int page, int size);
 
     Single<Set<Client>> search(String domain, String query);
 
     Single<Set<Client>> findByDomain(String domain);
 
-    Single<Client> create(String domain, NewClient newClient);
+    Single<Page<Client>> findByDomain(String domain, int page, int size);
 
-    Single<Client> create(Client client);
+    Single<Set<Client>> findByDomainAndExtensionGrant(String domain, String tokenGranter);
 
-    /**
-     * Since Dynamic Client Registration, many new fields have been added to client.
-     * Using this legacy update method may make you loose data.
-     * You should better use patch(String,String,PatchClient) method.
-     */
-    @Deprecated
-    Single<Client> update(String domain, String id, UpdateClient updateClient);
+    Single<Set<Client>> findByCertificate(String certificate);
 
-    Single<Client> update(Client client);
+    Single<Set<Client>> findByIdentityProvider(String identityProvider);
 
-    default Single<Client> patch(String domain, String id, PatchClient patchClient) {
-        return patch(domain, id, patchClient, false);
-    }
+    Single<Set<TopClient>> findTopClients();
 
-    Single<Client> patch(String domain, String id, PatchClient patchClient, boolean forceNull);
+    Single<Set<TopClient>> findTopClientsByDomain(String domain);
 
-    Single<Set<Client>> findByIdentityProvider(String identityProvider);
+    Single<TotalClient> findTotalClients();
 
-    Single<Set<Client>> findByCertificate(String certificate);
+    Single<TotalClient> findTotalClientsByDomain(String domain);
 
-    Single<Set<Client>> findByDomainAndExtensionGrant(String domain, String tokenGranter);
+    Maybe<Client> findByDomainAndClientId(String domain, String clientId);
 
-    Single<Set<Client>> findAll();
+    Maybe<Client> findById(String id);
 
-    Single<Page<Client>> findAll(int page, int size);
+    Single<Client> create(String domain, NewClient newClient);
 
-    Single<Set<TopClient>> findTopClients();
+    Single<Client> create(Client client);
 
-    Single<Set<TopClient>> findTopClientsByDomain(String domain);
+    default Single<Client> patch(String domain, String id, PatchClient patchClient) {
+        return patch(domain, id, patchClient, false);
+    }
 
-    Single<TotalClient> findTotalClientsByDomain(String domain);
+    Single<Client> patch(String domain, String id, PatchClient patchClient, boolean forceNull);
 
-    Single<TotalClient> findTotalClients();
+    Single<Client> renewClientSecret(String domain, String id);
 
     Completable delete(String clientId);
+
+    Single<Client> update(Client client);
+
+    /**
+     * Since Dynamic Client Registration, many new fields have been added to client.
+     * Using this legacy update method may make you loose data.
+     * You should better use patch(String,String,PatchClient) method.
+     */
+    @Deprecated
+    Single<Client> update(String domain, String id, UpdateClient updateClient);
 }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
index 6ce18bf855..3fb6c138d8 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/ClientServiceImpl.java
@@ -431,6 +431,21 @@ public class ClientServiceImpl implements ClientService {
                 });
     }
 
+    @Override
+    public Single<Client> renewClientSecret(String domain, String id) {
+        LOGGER.debug("Renew client secret for client {} in domain {}", id, domain);
+        return clientRepository.findById(id)
+                .switchIfEmpty(Maybe.error(new ClientNotFoundException(id)))
+                .flatMapSingle(client -> {
+                    // update client secret
+                    client.setClientSecret(SecureRandomString.generate());
+
+                    // update client and reload domain
+                    return updateClientAndReloadDomain(domain, client);
+                })
+                .onErrorResumeNext(this::handleError);
+    }
+
     /**
      * <pre>
      * This function will return an error if :
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
index f9db07beda..8bef1ea70c 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.html
@@ -186,12 +186,19 @@
     </div>
   </form>
   <div class="client-settings-delete-zone" fxLayout="column">
-    <p>This operation cannot be undone.</p>
-    Client will be deleted, applications related to this client will no longer be able to request for tokens and access protected resources.
-    <div class="client-settings-delete-button" fxLayout="row">
-      <span fxFlex></span>
-      <button mat-raised-button color="warn" (click)="delete($event)">DELETE</button>
-    </div>
+    <mat-list>
+      <mat-list-item>
+        <h3 matLine>Renew client secret</h3>
+        <p matLine>All applications using this client secret will need to be updated to be able to request for tokens and access protected resources.</p>
+        <button mat-stroked-button color="warn" (click)="renewClientSecret($event)">RENEW</button>
+      </mat-list-item>
+      <mat-divider></mat-divider>
+      <mat-list-item>
+        <h3 matLine>Delete client</h3>
+        <p matLine>Client will be deleted, applications related to this client will no longer be able to request for tokens and access protected resources.</p>
+        <button mat-raised-button color="warn" (click)="delete($event)">DELETE</button>
+      </mat-list-item>
+    </mat-list>
   </div>
 </div>
 <div class="client-settings-description" fxFlex>
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
index db132c3ccd..de6e85e300 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.scss
@@ -9,14 +9,19 @@ form {
 }
 
 .client-settings-delete-zone {
-  color: #f44336;
-  font-size: 75%;
   margin-top: 20px;
   padding: 20px;
   border-radius: 2px;
   border: 1px solid #e2e5e7;
-  .client-settings-delete-button {
-    margin-top: 20px;
+
+  mat-list-item {
+    h3 {
+      color: #f44336;
+    }
+    p {
+      font-size: 75%;
+      color: grey;
+    }
   }
 }
 
diff --git a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
index 71ac769803..7ef445dcb3 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/clients/client/settings/settings.component.ts
@@ -324,4 +324,18 @@ export class ClientSettingsComponent implements OnInit {
         }
       });
   }
+
+  renewClientSecret(event) {
+    event.preventDefault();
+    this.dialogService
+      .confirm('Renew Client secret', 'Are you sure you want to renew the client secret ?')
+      .subscribe(res => {
+        if (res) {
+          this.clientService.renewClientSecret(this.domainId, this.client.id).map(res => res.json()).subscribe(data => {
+            this.client = data;
+            this.snackbarService.open("Client secret updated");
+          });
+        }
+      });
+  }
 }
diff --git a/gravitee-am-ui/src/app/services/client.service.ts b/gravitee-am-ui/src/app/services/client.service.ts
index c3096813d8..6f58c97b6f 100644
--- a/gravitee-am-ui/src/app/services/client.service.ts
+++ b/gravitee-am-ui/src/app/services/client.service.ts
@@ -64,4 +64,8 @@ export class ClientService {
   delete(domainId, id): Observable<Response> {
     return this.http.delete(this.clientsURL + domainId + "/clients/" + id);
   }
+
+  renewClientSecret(domainId, id): Observable<Response> {
+    return this.http.post(this.clientsURL + domainId + "/clients/" + id + "/secret/_renew", {});
+  }
 }
diff --git a/postman/graviteeam-collection.json b/postman/graviteeam-collection.json
index 4c871c1cab..618fcf2332 100644
--- a/postman/graviteeam-collection.json
+++ b/postman/graviteeam-collection.json
@@ -1,7 +1,7 @@
 {
 	"info": {
-		"_postman_id": "e320de06-8313-4716-9b82-f674ea61347c",
-		"name": "Gravitee.io AM copy",
+		"_postman_id": "0928edc9-9ae1-4d4c-ab6d-eeccf9d8370f",
+		"name": "Gravitee.io AM",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
 	"item": [
@@ -1219,6 +1219,409 @@
 				{
 					"name": "Client Credentials Grant",
 					"item": [
+						{
+							"name": "Case - renew client secret",
+							"item": [
+								{
+									"name": "Create client 3",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "fbe2362d-ad75-4baa-8825-b74218b047c7",
+												"exec": [
+													"pm.test(\"Status code is 201\", function () {",
+													"    pm.response.to.have.status(201);",
+													"});",
+													"",
+													"var body = JSON.parse(responseBody);",
+													"pm.environment.set('client3', body.id);"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"value": "application/json"
+											},
+											{
+												"key": "Authorization",
+												"value": "Bearer {{token}}"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n  \"clientId\": \"my-client-3\",\n  \"clientSecret\": \"my-client-secret-3\"\n}"
+										},
+										"url": {
+											"raw": "{{management_url}}/management/domains/test/clients",
+											"host": [
+												"{{management_url}}"
+											],
+											"path": [
+												"management",
+												"domains",
+												"test",
+												"clients"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Configure client 3",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "58987251-6ffb-479d-80af-40373ea5a0cb",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"// wait for sync process",
+													"setTimeout(function(){}, 5000);"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "PATCH",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Bearer {{token}} ",
+												"type": "text"
+											},
+											{
+												"key": "Content-Type",
+												"name": "Content-Type",
+												"value": "application/json",
+												"type": "text"
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": "{\n \"authorizedGrantTypes\": [\n    \"client_credentials\"\n  ]\n}"
+										},
+										"url": {
+											"raw": "{{management_url}}/management/domains/test/clients/{{client3}}",
+											"host": [
+												"{{management_url}}"
+											],
+											"path": [
+												"management",
+												"domains",
+												"test",
+												"clients",
+												"{{client3}}"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Generate token",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "462b93ec-a015-41b8-a1bf-8d5446fc31aa",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"pm.test(\"Has an access_token without refresh token\", function () {",
+													"    var body = pm.response.json();",
+													"    pm.expect(body).to.have.property('access_token');",
+													"    pm.expect(body).to.have.property('token_type');",
+													"    pm.expect(body.token_type).to.eql('bearer');",
+													"    pm.expect(body).to.have.property('expires_in');",
+													"    pm.expect(body).to.not.have.property('refresh_token');",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Basic bXktY2xpZW50LTM6bXktY2xpZW50LXNlY3JldC0z"
+											}
+										],
+										"body": {
+											"mode": "urlencoded",
+											"urlencoded": [
+												{
+													"key": "grant_type",
+													"value": "client_credentials",
+													"type": "text"
+												}
+											]
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/token",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"token"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Renew client 3 secret",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "58987251-6ffb-479d-80af-40373ea5a0cb",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});",
+													"",
+													"var body = JSON.parse(responseBody);",
+													"pm.environment.set('client3Secret', body.clientSecret);",
+													"",
+													"// wait for sync process",
+													"setTimeout(function(){}, 5000);"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{token}} "
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{management_url}}/management/domains/test/clients/{{client3}}/secret/_renew",
+											"host": [
+												"{{management_url}}"
+											],
+											"path": [
+												"management",
+												"domains",
+												"test",
+												"clients",
+												"{{client3}}",
+												"secret",
+												"_renew"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Generate token - wrong secret",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "462b93ec-a015-41b8-a1bf-8d5446fc31aa",
+												"exec": [
+													"pm.test(\"Status code is 401\", function () {",
+													"    pm.response.to.have.status(401);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Authorization",
+												"value": "Basic bXktY2xpZW50LTM6bXktY2xpZW50LXNlY3JldC0z"
+											}
+										],
+										"body": {
+											"mode": "urlencoded",
+											"urlencoded": [
+												{
+													"key": "grant_type",
+													"value": "client_credentials",
+													"type": "text"
+												}
+											]
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/token",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"token"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Generate token - new secret",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "462b93ec-a015-41b8-a1bf-8d5446fc31aa",
+												"exec": [
+													"pm.test(\"Status code is 200\", function () {",
+													"    pm.response.to.have.status(200);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										},
+										{
+											"listen": "prerequest",
+											"script": {
+												"id": "904f0113-b2b9-4645-9511-48bb10791506",
+												"exec": [
+													""
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "POST",
+										"header": [
+											{
+												"key": "Content-Type",
+												"name": "Content-Type",
+												"value": "application/x-www-form-urlencoded",
+												"type": "text"
+											}
+										],
+										"body": {
+											"mode": "urlencoded",
+											"urlencoded": [
+												{
+													"key": "grant_type",
+													"value": "client_credentials",
+													"type": "text"
+												},
+												{
+													"key": "client_id",
+													"value": "my-client-3",
+													"type": "text"
+												},
+												{
+													"key": "client_secret",
+													"value": "{{client3Secret}}",
+													"type": "text"
+												}
+											]
+										},
+										"url": {
+											"raw": "{{gateway_url}}/test/oauth/token",
+											"host": [
+												"{{gateway_url}}"
+											],
+											"path": [
+												"test",
+												"oauth",
+												"token"
+											]
+										}
+									},
+									"response": []
+								},
+								{
+									"name": "Delete client 3",
+									"event": [
+										{
+											"listen": "test",
+											"script": {
+												"id": "58987251-6ffb-479d-80af-40373ea5a0cb",
+												"exec": [
+													"pm.test(\"Status code is 204\", function () {",
+													"    pm.response.to.have.status(204);",
+													"});"
+												],
+												"type": "text/javascript"
+											}
+										}
+									],
+									"request": {
+										"method": "DELETE",
+										"header": [
+											{
+												"key": "Authorization",
+												"type": "text",
+												"value": "Bearer {{token}} "
+											}
+										],
+										"body": {
+											"mode": "raw",
+											"raw": ""
+										},
+										"url": {
+											"raw": "{{management_url}}/management/domains/test/clients/{{client3}}",
+											"host": [
+												"{{management_url}}"
+											],
+											"path": [
+												"management",
+												"domains",
+												"test",
+												"clients",
+												"{{client3}}"
+											]
+										}
+									},
+									"response": []
+								}
+							],
+							"_postman_isSubFolder": true
+						},
 						{
 							"name": "Invalid client",
 							"event": [


Test patch:
diff --git a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/ClientResourceTest.java b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/ClientResourceTest.java
index 5751f9b5db..9ff127ecbe 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/ClientResourceTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-handlers/gravitee-am-management-api-handlers-management/src/test/java/io/gravitee/am/management/handlers/management/api/resources/ClientResourceTest.java
@@ -18,8 +18,10 @@ package io.gravitee.am.management.handlers.management.api.resources;
 import io.gravitee.am.management.handlers.management.api.JerseySpringTest;
 import io.gravitee.am.model.Client;
 import io.gravitee.am.model.Domain;
+import io.gravitee.am.service.exception.ClientNotFoundException;
 import io.gravitee.common.http.HttpStatusCode;
 import io.reactivex.Maybe;
+import io.reactivex.Single;
 import org.junit.Test;
 
 import javax.ws.rs.core.Response;
@@ -101,4 +103,54 @@ public class ClientResourceTest extends JerseySpringTest {
         final Response response = target("domains").path(domainId).path("clients").path(clientId).request().get();
         assertEquals(HttpStatusCode.BAD_REQUEST_400, response.getStatus());
     }
+
+    @Test
+    public void shouldRenewClientSecret() {
+        final String domainId = "domain-id";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final String clientId = "client-id";
+        final Client mockClient = new Client();
+        mockClient.setId(clientId);
+        mockClient.setClientId("client-name");
+        mockClient.setDomain(domainId);
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Single.just(mockClient)).when(clientService).renewClientSecret(domainId, clientId);
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("clients")
+                .path(clientId)
+                .path("secret/_renew")
+                .request()
+                .post(null);
+        assertEquals(HttpStatusCode.OK_200, response.getStatus());
+    }
+
+    @Test
+    public void shouldRenewClientSecret_clientNotFound() {
+        final String domainId = "domain-id";
+        final Domain mockDomain = new Domain();
+        mockDomain.setId(domainId);
+
+        final String clientId = "client-id";
+        final Client mockClient = new Client();
+        mockClient.setId(clientId);
+        mockClient.setClientId("client-name");
+        mockClient.setDomain(domainId);
+
+        doReturn(Maybe.just(mockDomain)).when(domainService).findById(domainId);
+        doReturn(Single.error(new ClientNotFoundException(clientId))).when(clientService).renewClientSecret(domainId, clientId);
+
+        final Response response = target("domains")
+                .path(domainId)
+                .path("clients")
+                .path(clientId)
+                .path("secret/_renew")
+                .request()
+                .post(null);
+        assertEquals(HttpStatusCode.NOT_FOUND_404, response.getStatus());
+    }
 }
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
index 3682f8fb23..03e091a713 100644
--- a/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/ClientServiceTest.java
@@ -730,4 +730,46 @@ public class ClientServiceTest {
         verify(clientRepository, times(1)).findById(anyString());
         verify(clientRepository, times(1)).update(any(Client.class));
     }
+
+    @Test
+    public void shouldRenewSecret() {
+        when(domainService.reload(eq(DOMAIN), any())).thenReturn(Single.just(new Domain()));
+        when(clientRepository.findById("my-client")).thenReturn(Maybe.just(new Client()));
+        when(clientRepository.update(any(Client.class))).thenReturn(Single.just(new Client()));
+
+        TestObserver testObserver = clientService.renewClientSecret(DOMAIN, "my-client").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+
+        verify(clientRepository, times(1)).findById(anyString());
+        verify(clientRepository, times(1)).update(any(Client.class));
+    }
+
+    @Test
+    public void shouldRenewSecret_clientNotFound() {
+        when(clientRepository.findById("my-client")).thenReturn(Maybe.empty());
+
+        TestObserver testObserver = clientService.renewClientSecret(DOMAIN, "my-client").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertError(ClientNotFoundException.class);
+        testObserver.assertNotComplete();
+
+        verify(clientRepository, never()).update(any());
+    }
+
+    @Test
+    public void shouldRenewSecret_technicalException() {
+        when(clientRepository.findById("my-client")).thenReturn(Maybe.error(TechnicalException::new));
+
+        TestObserver testObserver = clientService.renewClientSecret(DOMAIN, "my-client").test();
+        testObserver.awaitTerminalEvent();
+
+        testObserver.assertError(TechnicalManagementException.class);
+        testObserver.assertNotComplete();
+
+        verify(clientRepository, never()).update(any());
+    }
 }