Remove KeycloakInstalled
Signed-off-by: Douglas Palmer <dpalmer@redhat.com> Closes #28790
This commit is contained in:
parent
b2f09feebf
commit
eae20c76bd
6 changed files with 0 additions and 779 deletions
|
@ -1,86 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!--
|
|
||||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
~ and other contributors as indicated by the @author tags.
|
|
||||||
~
|
|
||||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
~ you may not use this file except in compliance with the License.
|
|
||||||
~ You may obtain a copy of the License at
|
|
||||||
~
|
|
||||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
~
|
|
||||||
~ Unless required by applicable law or agreed to in writing, software
|
|
||||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
~ See the License for the specific language governing permissions and
|
|
||||||
~ limitations under the License.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
|
||||||
<parent>
|
|
||||||
<artifactId>keycloak-parent</artifactId>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<version>999.0.0-SNAPSHOT</version>
|
|
||||||
<relativePath>../../../pom.xml</relativePath>
|
|
||||||
</parent>
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<artifactId>keycloak-installed-adapter</artifactId>
|
|
||||||
<name>Keycloak Installed Application</name>
|
|
||||||
<description/>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-adapter-spi</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-adapter-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bouncycastle</groupId>
|
|
||||||
<artifactId>bcprov-jdk18on</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.httpcomponents</groupId>
|
|
||||||
<artifactId>httpclient</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jboss.logging</groupId>
|
|
||||||
<artifactId>jboss-logging</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jboss.logging</groupId>
|
|
||||||
<artifactId>commons-logging-jboss-logging</artifactId>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jboss.resteasy</groupId>
|
|
||||||
<artifactId>resteasy-client</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jboss.spec.javax.ws.rs</groupId>
|
|
||||||
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.undertow</groupId>
|
|
||||||
<artifactId>undertow-core</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
|
@ -1,522 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.adapters.installed;
|
|
||||||
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ForkJoinPool;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.OAuthErrorException;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
|
||||||
import org.keycloak.adapters.ServerRequest;
|
|
||||||
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
|
||||||
import org.keycloak.common.util.SecretGenerator;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
|
||||||
import org.keycloak.representations.IDToken;
|
|
||||||
|
|
||||||
import io.undertow.Handlers;
|
|
||||||
import io.undertow.Undertow;
|
|
||||||
import io.undertow.server.HttpHandler;
|
|
||||||
import io.undertow.server.HttpServerExchange;
|
|
||||||
import io.undertow.server.handlers.AllowedMethodsHandler;
|
|
||||||
import io.undertow.server.handlers.GracefulShutdownHandler;
|
|
||||||
import io.undertow.server.handlers.PathHandler;
|
|
||||||
import io.undertow.util.Headers;
|
|
||||||
import io.undertow.util.Methods;
|
|
||||||
import io.undertow.util.StatusCodes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
public class KeycloakInstalled {
|
|
||||||
private static final String KEYCLOAK_JSON = "META-INF/keycloak.json";
|
|
||||||
|
|
||||||
private KeycloakDeployment deployment;
|
|
||||||
|
|
||||||
private enum Status {
|
|
||||||
LOGGED_MANUAL, LOGGED_DESKTOP
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* local port to listen for callbacks. The value {@code 0} will choose a random port.
|
|
||||||
*/
|
|
||||||
private int listenPort = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* local hostname to listen for callbacks.
|
|
||||||
*/
|
|
||||||
private String listenHostname = "localhost";
|
|
||||||
|
|
||||||
private AccessTokenResponse tokenResponse;
|
|
||||||
private String tokenString;
|
|
||||||
private String idTokenString;
|
|
||||||
private IDToken idToken;
|
|
||||||
private AccessToken token;
|
|
||||||
private String refreshToken;
|
|
||||||
private Status status;
|
|
||||||
private Locale locale;
|
|
||||||
private ResteasyClient resteasyClient;
|
|
||||||
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
|
|
||||||
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
|
|
||||||
Pattern codePattern = Pattern.compile("code=([^&]+)");
|
|
||||||
private CallbackListener callback;
|
|
||||||
private DesktopProvider desktopProvider = new DesktopProvider();
|
|
||||||
|
|
||||||
|
|
||||||
public KeycloakInstalled() {
|
|
||||||
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
|
|
||||||
deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeycloakInstalled(InputStream config) {
|
|
||||||
deployment = KeycloakDeploymentBuilder.build(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeycloakInstalled(KeycloakDeployment deployment) {
|
|
||||||
this.deployment = deployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResteasyClient(ResteasyClient resteasyClient) {
|
|
||||||
this.resteasyClient = resteasyClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Locale getLocale() {
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocale(Locale locale) {
|
|
||||||
this.locale = locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getListenPort() {
|
|
||||||
return listenPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the local port to listen for callbacks. The value {@code 0} will choose a random port. Defaults to {@code 0}.
|
|
||||||
* @param listenPort a valid port number
|
|
||||||
*/
|
|
||||||
public void setListenPort(int listenPort) {
|
|
||||||
if (listenPort < 0 || listenPort > 65535) {
|
|
||||||
throw new IllegalArgumentException("localPort");
|
|
||||||
}
|
|
||||||
this.listenPort = listenPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getListenHostname() {
|
|
||||||
return listenHostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the local hostname to listen for callbacks. The value {@code 0} will choose a random port
|
|
||||||
* @param listenHostname a valid local hostname
|
|
||||||
*/
|
|
||||||
public void setListenHostname(String listenHostname) {
|
|
||||||
this.listenHostname = listenHostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void login() throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException {
|
|
||||||
if (isDesktopSupported()) {
|
|
||||||
loginDesktop();
|
|
||||||
} else {
|
|
||||||
loginManual();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void login(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException, InterruptedException, OAuthErrorException, URISyntaxException {
|
|
||||||
if (isDesktopSupported()) {
|
|
||||||
loginDesktop();
|
|
||||||
} else {
|
|
||||||
loginManual(printer, reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void logout() throws IOException, InterruptedException, URISyntaxException {
|
|
||||||
if (status == Status.LOGGED_DESKTOP) {
|
|
||||||
logoutDesktop();
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenString = null;
|
|
||||||
token = null;
|
|
||||||
|
|
||||||
idTokenString = null;
|
|
||||||
idToken = null;
|
|
||||||
|
|
||||||
refreshToken = null;
|
|
||||||
|
|
||||||
status = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginDesktop() throws IOException, VerificationException, OAuthErrorException, URISyntaxException, ServerRequest.HttpFailure, InterruptedException {
|
|
||||||
callback = new CallbackListener();
|
|
||||||
callback.start();
|
|
||||||
|
|
||||||
String redirectUri = getRedirectUri(callback);
|
|
||||||
String state = UUID.randomUUID().toString();
|
|
||||||
Pkce pkce = deployment.isPkce() ? generatePkce() : null;
|
|
||||||
|
|
||||||
String authUrl = createAuthUrl(redirectUri, state, pkce);
|
|
||||||
|
|
||||||
desktopProvider.browse(new URI(authUrl));
|
|
||||||
|
|
||||||
try {
|
|
||||||
callback.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
callback.stop();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback.error != null) {
|
|
||||||
throw new OAuthErrorException(callback.error, callback.errorDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.equals(callback.state)) {
|
|
||||||
throw new VerificationException("Invalid state");
|
|
||||||
}
|
|
||||||
|
|
||||||
processCode(callback.code, redirectUri, pkce);
|
|
||||||
|
|
||||||
status = Status.LOGGED_DESKTOP;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
if (callback != null) {
|
|
||||||
callback.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String createAuthUrl(String redirectUri, String state, Pkce pkce) {
|
|
||||||
|
|
||||||
KeycloakUriBuilder builder = deployment.getAuthUrl().clone()
|
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
|
||||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID);
|
|
||||||
|
|
||||||
if (state != null) {
|
|
||||||
builder.queryParam(OAuth2Constants.STATE, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale != null) {
|
|
||||||
builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkce != null) {
|
|
||||||
builder.queryParam(OAuth2Constants.CODE_CHALLENGE, pkce.getCodeChallenge());
|
|
||||||
builder.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, "S256");
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Pkce generatePkce(){
|
|
||||||
return Pkce.generatePkce();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException {
|
|
||||||
CallbackListener callback = new CallbackListener();
|
|
||||||
callback.start();
|
|
||||||
|
|
||||||
String redirectUri = getRedirectUri(callback);
|
|
||||||
|
|
||||||
// pass the id_token_hint so that sessions is invalidated for this particular session
|
|
||||||
String logoutUrl = deployment.getLogoutUrl().clone()
|
|
||||||
.queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri)
|
|
||||||
.queryParam("id_token_hint", idTokenString)
|
|
||||||
.build().toString();
|
|
||||||
|
|
||||||
desktopProvider.browse(new URI(logoutUrl));
|
|
||||||
|
|
||||||
try {
|
|
||||||
callback.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
callback.stop();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRedirectUri(CallbackListener callback) {
|
|
||||||
return String.format("http://%s:%s", getListenHostname(), callback.getLocalPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginManual() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
|
||||||
loginManual(System.out, new InputStreamReader(System.in));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
|
||||||
|
|
||||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
|
||||||
|
|
||||||
Pkce pkce = generatePkce();
|
|
||||||
|
|
||||||
String authUrl = createAuthUrl(redirectUri, null, pkce);
|
|
||||||
|
|
||||||
printer.println("Open the following URL in a browser. After login copy/paste the code back and press <enter>");
|
|
||||||
printer.println(authUrl);
|
|
||||||
printer.println();
|
|
||||||
printer.print("Code: ");
|
|
||||||
|
|
||||||
String code = readCode(reader);
|
|
||||||
processCode(code, redirectUri, pkce);
|
|
||||||
|
|
||||||
status = Status.LOGGED_MANUAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenString() {
|
|
||||||
return tokenString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenString(long minValidity, TimeUnit unit) throws VerificationException, IOException, ServerRequest.HttpFailure {
|
|
||||||
long expires = ((long) token.getExp()) * 1000 - unit.toMillis(minValidity);
|
|
||||||
if (expires < System.currentTimeMillis()) {
|
|
||||||
refreshToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshToken() throws IOException, ServerRequest.HttpFailure, VerificationException {
|
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
|
||||||
parseAccessToken(tokenResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
|
|
||||||
parseAccessToken(tokenResponse);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseAccessToken(AccessTokenResponse tokenResponse) throws VerificationException {
|
|
||||||
this.tokenResponse = tokenResponse;
|
|
||||||
tokenString = tokenResponse.getToken();
|
|
||||||
refreshToken = tokenResponse.getRefreshToken();
|
|
||||||
idTokenString = tokenResponse.getIdToken();
|
|
||||||
|
|
||||||
AdapterTokenVerifier.VerifiedTokens tokens = AdapterTokenVerifier.verifyTokens(tokenString, idTokenString, deployment);
|
|
||||||
token = tokens.getAccessToken();
|
|
||||||
idToken = tokens.getIdToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessToken getToken() {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDToken getIdToken() {
|
|
||||||
return idToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getIdTokenString() {
|
|
||||||
return idTokenString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRefreshToken() {
|
|
||||||
return refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AccessTokenResponse getTokenResponse() {
|
|
||||||
return tokenResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDesktopProvider(DesktopProvider desktopProvider) {
|
|
||||||
this.desktopProvider = desktopProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDesktopSupported() {
|
|
||||||
return desktopProvider.isDesktopSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeycloakDeployment getDeployment() {
|
|
||||||
return deployment;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processCode(String code, String redirectUri, Pkce pkce) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
|
||||||
|
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null, pkce == null ? null : pkce.getCodeVerifier());
|
|
||||||
parseAccessToken(tokenResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readCode(Reader reader) throws IOException {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
char cb[] = new char[1];
|
|
||||||
while (reader.read(cb) != -1) {
|
|
||||||
char c = cb[0];
|
|
||||||
if ((c == ' ') || (c == '\n') || (c == '\r')) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
sb.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
class CallbackListener implements HttpHandler {
|
|
||||||
private final CountDownLatch shutdownSignal = new CountDownLatch(1);
|
|
||||||
|
|
||||||
private String code;
|
|
||||||
private String error;
|
|
||||||
private String errorDescription;
|
|
||||||
private String state;
|
|
||||||
private Undertow server;
|
|
||||||
|
|
||||||
private GracefulShutdownHandler gracefulShutdownHandler;
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
PathHandler pathHandler = Handlers.path().addExactPath("/", this);
|
|
||||||
AllowedMethodsHandler allowedMethodsHandler = new AllowedMethodsHandler(pathHandler, Methods.GET);
|
|
||||||
gracefulShutdownHandler = Handlers.gracefulShutdown(allowedMethodsHandler);
|
|
||||||
|
|
||||||
server = Undertow.builder()
|
|
||||||
.setIoThreads(1)
|
|
||||||
.setWorkerThreads(1)
|
|
||||||
.addHttpListener(getListenPort(), getListenHostname())
|
|
||||||
.setHandler(gracefulShutdownHandler)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
server.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
try {
|
|
||||||
server.stop();
|
|
||||||
} catch (Exception ignore) {
|
|
||||||
// it is OK to happen if thread is modified while stopping the server, specially when a security manager is enabled
|
|
||||||
}
|
|
||||||
shutdownSignal.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLocalPort() {
|
|
||||||
return ((InetSocketAddress) server.getListenerInfo().get(0).getAddress()).getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void await() throws InterruptedException {
|
|
||||||
shutdownSignal.await();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleRequest(HttpServerExchange exchange) throws Exception {
|
|
||||||
gracefulShutdownHandler.shutdown();
|
|
||||||
|
|
||||||
if (!exchange.getQueryParameters().isEmpty()) {
|
|
||||||
readQueryParameters(exchange);
|
|
||||||
}
|
|
||||||
|
|
||||||
exchange.setStatusCode(StatusCodes.FOUND);
|
|
||||||
exchange.getResponseHeaders().add(Headers.LOCATION, getRedirectUrl());
|
|
||||||
exchange.endExchange();
|
|
||||||
|
|
||||||
shutdownSignal.countDown();
|
|
||||||
|
|
||||||
ForkJoinPool.commonPool().execute(this::stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readQueryParameters(HttpServerExchange exchange) {
|
|
||||||
code = getQueryParameterIfPresent(exchange, OAuth2Constants.CODE);
|
|
||||||
error = getQueryParameterIfPresent(exchange, OAuth2Constants.ERROR);
|
|
||||||
errorDescription = getQueryParameterIfPresent(exchange, OAuth2Constants.ERROR_DESCRIPTION);
|
|
||||||
state = getQueryParameterIfPresent(exchange, OAuth2Constants.STATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getQueryParameterIfPresent(HttpServerExchange exchange, String name) {
|
|
||||||
Map<String, Deque<String>> queryParameters = exchange.getQueryParameters();
|
|
||||||
return queryParameters.containsKey(name) ? queryParameters.get(name).getFirst() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRedirectUrl() {
|
|
||||||
String redirectUrl = deployment.getTokenUrl().replace("/token", "/delegated");
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
redirectUrl += "?error=true";
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirectUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Pkce {
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-4.1
|
|
||||||
public static final int PKCE_CODE_VERIFIER_MAX_LENGTH = 128;
|
|
||||||
|
|
||||||
private final String codeChallenge;
|
|
||||||
private final String codeVerifier;
|
|
||||||
|
|
||||||
public Pkce(String codeVerifier, String codeChallenge) {
|
|
||||||
this.codeChallenge = codeChallenge;
|
|
||||||
this.codeVerifier = codeVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCodeChallenge() {
|
|
||||||
return codeChallenge;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCodeVerifier() {
|
|
||||||
return codeVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Pkce generatePkce() {
|
|
||||||
try {
|
|
||||||
String codeVerifier = SecretGenerator.getInstance().randomString(PKCE_CODE_VERIFIER_MAX_LENGTH);
|
|
||||||
String codeChallenge = generateS256CodeChallenge(codeVerifier);
|
|
||||||
return new Pkce(codeVerifier, codeChallenge);
|
|
||||||
} catch (Exception ex){
|
|
||||||
throw new RuntimeException("Could not generate PKCE", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-4.6
|
|
||||||
private static String generateS256CodeChallenge(String codeVerifier) throws Exception {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
md.update(codeVerifier.getBytes(StandardCharsets.ISO_8859_1));
|
|
||||||
return Base64Url.encode(md.digest());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class DesktopProvider {
|
|
||||||
public boolean isDesktopSupported() {
|
|
||||||
return Desktop.isDesktopSupported();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void browse(URI uri) throws IOException {
|
|
||||||
Desktop.getDesktop().browse(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -32,7 +32,6 @@
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>adapter-core</module>
|
<module>adapter-core</module>
|
||||||
<module>installed</module>
|
|
||||||
<module>jaxrs-oauth-client</module>
|
<module>jaxrs-oauth-client</module>
|
||||||
<module>js</module>
|
<module>js</module>
|
||||||
<module>servlet-filter</module>
|
<module>servlet-filter</module>
|
||||||
|
|
|
@ -1,164 +0,0 @@
|
||||||
[[_installed_adapter]]
|
|
||||||
==== CLI / Desktop Applications
|
|
||||||
|
|
||||||
{project_name} supports securing desktop
|
|
||||||
(for example Swing, JavaFX) or CLI applications via the
|
|
||||||
`KeycloakInstalled` adapter by performing the authentication step via the system browser.
|
|
||||||
|
|
||||||
The `KeycloakInstalled` adapter supports a `desktop` and a `manual`
|
|
||||||
variant. The desktop variant uses the system browser
|
|
||||||
to gather the user credentials. The manual variant
|
|
||||||
reads the user credentials from `STDIN`.
|
|
||||||
|
|
||||||
===== How it works
|
|
||||||
|
|
||||||
To authenticate a user with the `desktop` variant the `KeycloakInstalled`
|
|
||||||
adapter opens a desktop browser window where a user uses the regular {project_name}
|
|
||||||
login pages to log in when the `loginDesktop()` method is called on the `KeycloakInstalled` object.
|
|
||||||
|
|
||||||
The login page URL is opened with redirect parameter
|
|
||||||
that points to a local `ServerSocket` listening on a free ephemeral port
|
|
||||||
on `localhost` which is started by the adapter.
|
|
||||||
|
|
||||||
After a successful login the `KeycloakInstalled` receives the authorization code
|
|
||||||
from the incoming HTTP request and performs the authorization code flow.
|
|
||||||
Once the code to token exchange is completed the `ServerSocket` is shutdown.
|
|
||||||
|
|
||||||
TIP: If the user already has an active {project_name} session then
|
|
||||||
the login form is not shown but the code to token exchange is continued,
|
|
||||||
which enables a smooth Web based SSO experience.
|
|
||||||
|
|
||||||
The client eventually receives the tokens (access_token, refresh_token,
|
|
||||||
id_token) which can then be used to call backend services.
|
|
||||||
|
|
||||||
The `KeycloakInstalled` adapter provides support for renewal of stale tokens.
|
|
||||||
|
|
||||||
[[_installed_adapter_installation]]
|
|
||||||
===== Installing the adapter
|
|
||||||
|
|
||||||
[source,xml,subs="attributes+"]
|
|
||||||
----
|
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-installed-adapter</artifactId>
|
|
||||||
<version>{project_versionMvn}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
|
|
||||||
===== Client configuration
|
|
||||||
|
|
||||||
The application needs to be configured as a `public` OpenID Connect client with
|
|
||||||
`Standard Flow Enabled` and pass:[http://localhost] as an allowed `Valid Redirect URI`.
|
|
||||||
|
|
||||||
TIP: The `KeycloakInstalled` adapter supports the `PKCE` [RFC 7636] mechanism to provide additional protection during
|
|
||||||
code to token exchanges in the `OIDC` protocol. PKCE can be enabled with the `"enable-pkce": true` setting
|
|
||||||
in the adapter configuration. Enabling PKCE is highly recommended, to avoid code injection and code replay attacks.
|
|
||||||
|
|
||||||
===== Usage
|
|
||||||
|
|
||||||
The `KeycloakInstalled` adapter reads its configuration from
|
|
||||||
`META-INF/keycloak.json` on the classpath. Custom configurations
|
|
||||||
can be supplied with an `InputStream` or a `KeycloakDeployment`
|
|
||||||
through the `KeycloakInstalled` constructor.
|
|
||||||
|
|
||||||
In the example below, the client configuration for `desktop-app`
|
|
||||||
uses the following `keycloak.json`:
|
|
||||||
|
|
||||||
[source,json,subs="attributes+"]
|
|
||||||
----
|
|
||||||
|
|
||||||
{
|
|
||||||
"realm": "desktop-app-auth",
|
|
||||||
"auth-server-url": "http://localhost:8081{kc_base_path}",
|
|
||||||
"ssl-required": "external",
|
|
||||||
"resource": "desktop-app",
|
|
||||||
"public-client": true,
|
|
||||||
"use-resource-role-mappings": true,
|
|
||||||
"enable-pkce": true
|
|
||||||
}
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
the following sketch demonstrates working with the `KeycloakInstalled` adapter:
|
|
||||||
[source,java]
|
|
||||||
----
|
|
||||||
|
|
||||||
// reads the configuration from classpath: META-INF/keycloak.json
|
|
||||||
KeycloakInstalled keycloak = new KeycloakInstalled();
|
|
||||||
|
|
||||||
// opens desktop browser
|
|
||||||
keycloak.loginDesktop();
|
|
||||||
|
|
||||||
AccessToken token = keycloak.getToken();
|
|
||||||
// use token to send backend request
|
|
||||||
|
|
||||||
// ensure token is valid for at least 30 seconds
|
|
||||||
long minValidity = 30L;
|
|
||||||
String tokenString = keycloak.getTokenString(minValidity, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
|
|
||||||
// when you want to logout the user.
|
|
||||||
keycloak.logout();
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
TIP: The `KeycloakInstalled` class supports customization of the http responses returned by
|
|
||||||
login / logout requests via the `loginResponseWriter` and `logoutResponseWriter` attributes.
|
|
||||||
|
|
||||||
===== Example
|
|
||||||
|
|
||||||
The following provides an example for the configuration mentioned above.
|
|
||||||
|
|
||||||
[source,java]
|
|
||||||
----
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.keycloak.adapters.installed.KeycloakInstalled;
|
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
|
|
||||||
public class DesktopApp {
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
|
|
||||||
KeycloakInstalled keycloak = new KeycloakInstalled();
|
|
||||||
keycloak.setLocale(Locale.ENGLISH);
|
|
||||||
keycloak.loginDesktop();
|
|
||||||
|
|
||||||
AccessToken token = keycloak.getToken();
|
|
||||||
Executors.newSingleThreadExecutor().submit(() -> {
|
|
||||||
|
|
||||||
System.out.println("Logged in...");
|
|
||||||
System.out.println("Token: " + token.getSubject());
|
|
||||||
System.out.println("Username: " + token.getPreferredUsername());
|
|
||||||
try {
|
|
||||||
System.out.println("AccessToken: " + keycloak.getTokenString());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
int timeoutSeconds = 20;
|
|
||||||
System.out.printf("Logging out in...%d Seconds%n", timeoutSeconds);
|
|
||||||
try {
|
|
||||||
TimeUnit.SECONDS.sleep(timeoutSeconds);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
keycloak.logout();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println("Exiting...");
|
|
||||||
System.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
|
|
@ -23,7 +23,6 @@ endif::[]
|
||||||
|
|
||||||
ifeval::[{project_community}==true]
|
ifeval::[{project_community}==true]
|
||||||
include::jaas.adoc[]
|
include::jaas.adoc[]
|
||||||
include::installed-adapter.adoc[]
|
|
||||||
endif::[]
|
endif::[]
|
||||||
|
|
||||||
ifeval::[{project_community}==true]
|
ifeval::[{project_community}==true]
|
||||||
|
|
5
pom.xml
5
pom.xml
|
@ -1044,11 +1044,6 @@
|
||||||
<artifactId>keycloak-as7-adapter-spi</artifactId>
|
<artifactId>keycloak-as7-adapter-spi</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-installed-adapter</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-jaxrs-oauth-client</artifactId>
|
<artifactId>keycloak-jaxrs-oauth-client</artifactId>
|
||||||
|
|
Loading…
Reference in a new issue