Skip to content

Instantly share code, notes, and snippets.

@pmorch
Last active November 18, 2020 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmorch/1fe8f0ec842e9581696ae504a5d1c220 to your computer and use it in GitHub Desktop.
Save pmorch/1fe8f0ec842e9581696ae504a5d1c220 to your computer and use it in GitHub Desktop.
Workaround for scribejava: Keycloak: Adding a custom field for the `token` endpoint POST to get Single-Sign-Off to work #980
package dk.lessor.oauth2;
import com.github.scribejava.apis.KeycloakApi;
import com.github.scribejava.core.httpclient.HttpClient;
import com.github.scribejava.core.httpclient.HttpClientConfig;
import com.github.scribejava.core.oauth.OAuth20Service;
import java.io.OutputStream;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/** See description of KeycloakLogoutableOAuth20Service as to why this exists */
public class KeycloakLogoutableApi extends KeycloakApi {
private static final ConcurrentMap<String, KeycloakLogoutableApi> INSTANCES = new ConcurrentHashMap<>();
protected KeycloakLogoutableApi(String baseUrlWithRealm) {
super(baseUrlWithRealm);
}
public OAuth20Service createService(String apiKey, String apiSecret, String callback, String defaultScope,
String responseType, OutputStream debugStream, String userAgent, HttpClientConfig httpClientConfig,
HttpClient httpClient) {
return new KeycloakLogoutableOAuth20Service(this, apiKey, apiSecret, callback, defaultScope, responseType, debugStream, userAgent,
httpClientConfig, httpClient);
}
public static KeycloakApi instance(String baseUrl, String realm) {
final String defaultBaseUrlWithRealm = composeBaseUrlWithRealm(baseUrl, realm);
//java8: switch to ConcurrentMap::computeIfAbsent
KeycloakLogoutableApi api = INSTANCES.get(defaultBaseUrlWithRealm);
if (api == null) {
api = new KeycloakLogoutableApi(defaultBaseUrlWithRealm);
final KeycloakApi alreadyCreatedApi = INSTANCES.putIfAbsent(defaultBaseUrlWithRealm, api);
if (alreadyCreatedApi != null) {
return alreadyCreatedApi;
}
}
return api;
}
}
package dk.lessor.oauth2;
import com.github.scribejava.apis.KeycloakApi;
import com.github.scribejava.core.httpclient.HttpClient;
import com.github.scribejava.core.httpclient.HttpClientConfig;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.oauth.AccessTokenRequestParams;
import com.github.scribejava.core.oauth.OAuth20Service;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutionException;
/**
* We want to be able to have "instant" logout. For this to occur with keycloak,
* we need keycloak to call us with a AdminURL webhook. Until keycloak 12,
* this is the "old" keycloak-specific way of doing it.
*
* After the release of Keycloak 12, we'll be able to use an RFC based way of doing it instead. See:
* [KEYCLOAK-2940] OpenID Connect Back-Channel Logout
* https://issues.redhat.com/browse/KEYCLOAK-2940
*
* Unfortunately, for the legacy, pre-version-12 approach we need the token endpoint to be called
* with an extra, non-standard "client_session_state" parameter as described in:
* [KEYCLOAK-15234] Admin URL k_logout event is not always called when client logs out
* https://issues.redhat.com/browse/KEYCLOAK-15234. Beause of that we need to send the "client_session_state"
* parameter for Scribejava too, as described in:
* Keycloak: Adding a custom field for the `token` endpoint POST to get Single-Sign-Off to work
* https://github.com/scribejava/scribejava/issues/980
*
* That is what this class, KeycloakLogoutableOAuth20Service and its companion KeycloakLogoutableApi is for.
*
* To use it, use a KeycloakLogoutableApi instead of a KeycloakApi. So where
* https://github.com/scribejava/scribejava/wiki/getting-started says:
*
* final OAuth10aService service = new ServiceBuilder("your_api_key")
* .apiSecret("your_api_secret")
* .build(TwitterApi.instance());
*
* that would be this, for out-of-the-box KeycloakApi:
*
* final OAuth20Service service = new ServiceBuilder("your_api_key")
* .apiSecret("your_api_secret")
* .build(KeycloakApi.instance(baseURL, realm));
*
* Instead of KeycloakApi, use the KeycloakLogoutableApi.
*
* final OAuth20Service service = new ServiceBuilder("your_api_key")
* .apiSecret("your_api_secret")
* .build(KeycloakLogoutableApi.instance(baseURL, realm));
*
* Now, the OAuth20Service service returned by build() is really a KeycloakLogoutableOAuth20Service instance,
* so we can cast it as such and do:
*
* OAuth2AccessToken token = ((KeycloakLogoutableOAuth20Service) service).getAccessToken(code, sessionId);
*
* Done this way, Keycloak's AdminURL will be called when any client in the session logs out,
* achieving Single-Sign-Out
*
* This is not the most elegant approach, but it is the most elegant one I can think of that
* doesn't require *any* changes to ScribeJava
* (as decribed in https://github.com/scribejava/scribejava/issues/980)
*
* */
public class KeycloakLogoutableOAuth20Service extends OAuth20Service {
KeycloakLogoutableOAuth20Service(
KeycloakApi api,
String apiKey,
String apiSecret,
String callback,
String defaultScope,
String responseType,
OutputStream debugStream,
String userAgent,
HttpClientConfig httpClientConfig,
HttpClient httpClient) {
super(api, apiKey, apiSecret, callback, defaultScope, responseType, debugStream, userAgent,
httpClientConfig, httpClient);
}
OAuth2AccessToken getAccessToken(String code, String sessionId) throws IOException, InterruptedException, ExecutionException {
OAuthRequest request = createAccessTokenRequest(AccessTokenRequestParams.create(code));
request.addParameter("client_session_state", sessionId);
return sendAccessTokenRequestSync(request);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment