Skip to content

Commit

Permalink
Support logout with confidential client if grant_type=password is used
Browse files Browse the repository at this point in the history
  • Loading branch information
jkroepke committed Jan 12, 2022
1 parent fc0117f commit e47ec77
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 28 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Support logout with confidential client if grant_type=password is used.

### Changes

- Add `--import.validate` flag to disable pre validation checks inside keycloak-config-cli.
- Change maven wrapper to official one (https://maven.apache.org/wrapper/)

### Fixed

- Skip logout if grant_type=client_credentials is used

## [4.5.0] - 2021-12-19

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import net.jodah.failsafe.RetryPolicy;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.jboss.resteasy.client.jaxrs.internal.BasicAuthentication;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.slf4j.Logger;
Expand All @@ -37,8 +38,10 @@
import java.net.URL;
import java.text.MessageFormat;
import java.time.Duration;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;

/**
* This class exists cause we need to create a single keycloak instance or to close the keycloak before using a new one
Expand Down Expand Up @@ -166,17 +169,50 @@ public void close() {
}
}

// see: https://github.com/keycloak/keycloak/blob/8ea09d38168c22937363cf77a07f9de5dc7b48b0/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java#L207-L220

/**
* Logout a session via a non-browser invocation. Similar signature to refresh token except there is no grant_type.
* You must pass in the refresh token and
* authenticate the client if it is not public.
* <p>
* If the client is a confidential client
* you must include the client-id and secret in an Basic Auth Authorization header.
* <p>
* If the client is a public client, then you must include a "client_id" form parameter.
* <p>
* returns 204 if successful, 400 if not with a json error response.
*/
private void logout() {
ResteasyWebTarget target = resteasyClient
String refreshToken = this.keycloak.tokenManager().getAccessToken().getRefreshToken();
// if we do not have a refreshToken, we are not able ot logout (grant_type=client_credentials)
if (refreshToken == null) {
return;
}

ResteasyWebTarget resteasyWebTarget = resteasyClient
.target(properties.getUrl().toString())
.path("/realms/" + properties.getLoginRealm() + "/protocol/openid-connect/logout");

Form form = new Form();
form.param("refresh_token", refreshToken);

if (!properties.getClientId().isEmpty() && properties.getClientSecret().isEmpty()) {
form.param("client_id", properties.getClientId());
}

Form form = new Form()
.param("client_id", properties.getClientId())
.param("refresh_token", this.keycloak.tokenManager().getAccessToken().getRefreshToken());
if (!properties.getClientId().isEmpty() && !properties.getClientSecret().isEmpty()) {
resteasyWebTarget.register(new BasicAuthentication(properties.getClientId(), properties.getClientSecret()));
}

target.request().post(Entity.form(form));
Response response = resteasyWebTarget.request().post(Entity.form(form));
// if debugging is enabled, care about error on logout.
if (!response.getStatusInfo().equals(Response.Status.NO_CONTENT)) {
logger.warn("Unable to logout. HTTP Status: {}", response.getStatus());
if (logger.isDebugEnabled()) {
throw new WebApplicationException(response);
}
}
}

public boolean isClosed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package de.adorsys.keycloak.config.service;

import de.adorsys.keycloak.config.AbstractImportTest;
import de.adorsys.keycloak.config.provider.KeycloakProvider;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -50,69 +51,87 @@ void createServiceAccountInMasterRealm() throws IOException {
assertThat(realm.getRealm(), is(REALM_NAME));
assertThat(realm.isEnabled(), is(true));

ClientRepresentation client = keycloakRepository.getClient(
REALM_NAME,
"config-cli-master"
);
ClientRepresentation client = keycloakRepository.getClient(REALM_NAME, "config-cli-master");

assertThat(client.isServiceAccountsEnabled(), is(true));
}

@SuppressWarnings("SpringJavaAutowiredMembersInspection")
@Nested
@Order(1)
@TestPropertySource(properties = {
"keycloak.login-realm=service-account",
"keycloak.login-realm=master",
"keycloak.grant-type=client_credentials",
"keycloak.client-id=config-cli",
"keycloak.client-secret=config-cli-secret",
"keycloak.client-id=config-cli-master",
"keycloak.client-secret=config-cli-master-secret",
})
class ImportRealmUsingServiceAccountFromDifferentRealm {
class ImportRealmUsingServiceAccountFromMaster {
private static final String REALM_NAME = "service-account";

@Autowired
public RealmImportService realmImportService;


@Autowired
public KeycloakProvider keycloakProvider;

@Test
void updateExistingRealm() throws IOException {
doImport("02_update_realm_client_with_service_account_enabled.json", realmImportService);
@Order(1)
void createNewRealm() throws IOException {
doImport("01_create_realm_client_with_service_account_enabled.json", realmImportService);

RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).toRepresentation();

assertThat(realm.getRealm(), is(REALM_NAME));
assertThat(realm.isEnabled(), is(true));
assertThat(realm.getLoginTheme(), is("moped"));

ClientRepresentation client = keycloakRepository.getClient(REALM_NAME, "config-cli");

assertThat(client.isServiceAccountsEnabled(), is(true));
}

@Test
@Order(2)
void logout() {
keycloakProvider.close();
}
}

@SuppressWarnings("SpringJavaAutowiredMembersInspection")
@Nested
@Order(2)
@TestPropertySource(properties = {
"keycloak.login-realm=master",
"keycloak.login-realm=service-account",
"keycloak.grant-type=client_credentials",
"keycloak.client-id=config-cli-master",
"keycloak.client-secret=config-cli-master-secret",
"keycloak.client-id=config-cli",
"keycloak.client-secret=config-cli-secret",
})
class ImportRealmUsingServiceAccountFromMaster {
class ImportRealmUsingServiceAccountFromDifferentRealm {
private static final String REALM_NAME = "service-account";

@Autowired
public RealmImportService realmImportService;


@Autowired
public KeycloakProvider keycloakProvider;

@Test
void createNewRealm() throws IOException {
doImport("01_create_realm_client_with_service_account_enabled.json", realmImportService);
@Order(1)
void updateExistingRealm() throws IOException {
doImport("02_update_realm_client_with_service_account_enabled.json", realmImportService);

RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).toRepresentation();

assertThat(realm.getRealm(), is(REALM_NAME));
assertThat(realm.isEnabled(), is(true));
assertThat(realm.getLoginTheme(), is("moped"));
}

ClientRepresentation client = keycloakRepository.getClient(
REALM_NAME,
"config-cli"
);

assertThat(client.isServiceAccountsEnabled(), is(true));
@Test
@Order(2)
void logout() {
keycloakProvider.close();
}
}
}

0 comments on commit e47ec77

Please sign in to comment.