![JAR search and dependency download from the Maven repository](/logo.png)
com.canoo.dp.impl.security.KeycloakSecurity Maven / Gradle / Ivy
/*
* Copyright 2015-2018 Canoo Engineering AG.
*
* 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 com.canoo.dp.impl.security;
import com.canoo.dp.impl.platform.core.Assert;
import com.canoo.dp.impl.platform.core.http.HttpClientConnection;
import com.canoo.platform.client.ClientConfiguration;
import com.canoo.platform.client.PlatformClient;
import com.canoo.platform.client.security.Security;
import com.canoo.platform.core.DolphinRuntimeException;
import com.canoo.platform.core.PlatformConfiguration;
import com.canoo.platform.core.http.RequestMethod;
import com.google.gson.Gson;
import org.apiguardian.api.API;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static com.canoo.dp.impl.platform.core.http.HttpHeaderConstants.CONTENT_TYPE_HEADER;
import static com.canoo.dp.impl.platform.core.http.HttpHeaderConstants.FORM_MIME_TYPE;
import static com.canoo.dp.impl.platform.core.http.HttpHeaderConstants.TEXT_MIME_TYPE;
import static com.canoo.dp.impl.platform.core.http.HttpHeaderConstants.CHARSET;
import static com.canoo.dp.impl.platform.core.http.HttpStatus.SC_HTTP_UNAUTHORIZED;
import static com.canoo.dp.impl.security.SecurityConfiguration.APPLICATION_PROPERTY_NAME;
import static com.canoo.dp.impl.security.SecurityConfiguration.AUTH_ENDPOINT_PROPERTY_DEFAULT_VALUE;
import static com.canoo.dp.impl.security.SecurityConfiguration.AUTH_ENDPOINT_PROPERTY_NAME;
import static com.canoo.dp.impl.security.SecurityConfiguration.DIRECT_CONNECTION_PROPERTY_DEFAULT_VALUE;
import static com.canoo.dp.impl.security.SecurityConfiguration.DIRECT_CONNECTION_PROPERTY_NAME;
import static com.canoo.dp.impl.security.SecurityConfiguration.REALM_PROPERTY_NAME;
import static com.canoo.dp.impl.security.SecurityHttpHeader.APPLICATION_NAME_HEADER;
import static com.canoo.dp.impl.security.SecurityHttpHeader.REALM_NAME_HEADER;
import static org.apiguardian.api.API.Status.INTERNAL;
@API(since = "0.19.0", status = INTERNAL)
public class KeycloakSecurity implements Security {
private final static long TOKEN_EXPIRES_DELTA = 10_000;
private final static long MIN_TOKEN_EXPIRES_RUN = 30_000;
private final static Logger LOG = LoggerFactory.getLogger(KeycloakSecurity.class);
private final String authEndpoint;
private final String defaultRealmName;
private final String defaultAppName;
private final ExecutorService executor;
private final boolean directConnect;
private Future refreshTask;
private final Lock refreshLock = new ReentrantLock();
private final AtomicBoolean authorized;
private final AtomicReference accessToken;
private final Lock loginLogoutLock = new ReentrantLock();
public KeycloakSecurity(final ClientConfiguration configuration) {
Assert.requireNonNull(configuration, "configuration");
this.defaultAppName = configuration.getProperty(APPLICATION_PROPERTY_NAME);
this.authEndpoint = configuration.getProperty(AUTH_ENDPOINT_PROPERTY_NAME, AUTH_ENDPOINT_PROPERTY_DEFAULT_VALUE);
Assert.requireNonBlank(authEndpoint, "authEndpoint");
this.defaultRealmName = configuration.getProperty(REALM_PROPERTY_NAME);
this.directConnect = configuration.getBooleanProperty(DIRECT_CONNECTION_PROPERTY_NAME, DIRECT_CONNECTION_PROPERTY_DEFAULT_VALUE);
this.executor = configuration.getBackgroundExecutor();
Assert.requireNonNull(executor, "executor");
this.authorized = new AtomicBoolean(false);
this.accessToken = new AtomicReference<>(null);
}
@Override
public Future login(final String user, final String password) {
return login(user, password, PlatformConfiguration.empty());
}
@Override
public Future login(final String user, final String password, final PlatformConfiguration securityConfig) {
Assert.requireNonNull(securityConfig, "securityConfig");
return executor.submit(() -> {
loginLogoutLock.lock();
try {
if (authorized.get()) {
throw new DolphinRuntimeException("Already logged in!");
}
final String realmName = securityConfig.getProperty(REALM_PROPERTY_NAME, defaultRealmName);
final String appName = securityConfig.getProperty(APPLICATION_PROPERTY_NAME, defaultAppName);
try {
final String encodedUser = encode(user);
final String encodedPassword = encode(password);
final String encodedAppName = encode(appName);
final KeycloakOpenidConnectResult connectResult = receiveTokenByLogin(encodedUser, encodedPassword, realmName, appName);
accessToken.set(connectResult.getAccess_token());
authorized.set(true);
startTokenRefreshRunner(connectResult, realmName, encodedAppName);
} catch (final IOException | URISyntaxException e) {
throw new DolphinRuntimeException("Can not receive security token!", e);
}
} finally {
loginLogoutLock.unlock();
}
}, null);
}
@Override
public Future logout() {
return executor.submit(() -> {
loginLogoutLock.lock();
try {
authorized.set(false);
refreshLock.lock();
try {
refreshTask.cancel(true);
} finally {
refreshLock.unlock();
}
accessToken.set(null);
} finally {
loginLogoutLock.unlock();
}
}, null);
}
@Override
public boolean isAuthorized() {
//REST Endpoint at Keycloak site -> Call must be done manually
//http://www.keycloak.org/docs-api/3.3/rest-api/index.html
//See /admin/realms/{realm}/users/{id}/sessions
return authorized.get();
}
@Override
public String getAccessToken() {
return accessToken.get();
}
private String encode(final String value) throws UnsupportedEncodingException {
if (value != null) {
return URLEncoder.encode(value, CHARSET);
} else {
return value;
}
}
private void startTokenRefreshRunner(final KeycloakOpenidConnectResult connectResult, final String realmName, final String appName) {
refreshLock.lock();
try {
Assert.requireNonNull(connectResult, "connectResult");
refreshTask = executor.submit(() -> {
try {
final AtomicReference connectResultReference = new AtomicReference<>(connectResult);
while (!Thread.interrupted()) {
final KeycloakOpenidConnectResult currentConnectResult = connectResultReference.get();
Assert.requireNonNull(currentConnectResult, "currentConnectResult");
final long sleepTime = Math.max(MIN_TOKEN_EXPIRES_RUN, currentConnectResult.getExpires_in() - TOKEN_EXPIRES_DELTA);
Thread.sleep(sleepTime);
LOG.debug("Token refresh started");
final KeycloakOpenidConnectResult newConnectResult = receiveTokenByRefresh(currentConnectResult.getRefresh_token(), realmName, appName);
Assert.requireNonNull(newConnectResult, "newConnectResult");
accessToken.set(newConnectResult.getAccess_token());
LOG.debug("Token refresh done");
connectResultReference.set(newConnectResult);
}
} catch (final InterruptedException e) {
LOG.debug("Token refresh runner stopped");
} catch (final IOException | URISyntaxException e) {
throw new DolphinRuntimeException("Can not receive security token!", e);
}
}, null);
} finally {
refreshLock.unlock();
}
}
private HttpClientConnection createDirectConnection(final String realmName) throws URISyntaxException, IOException {
final URI url = new URI(authEndpoint + "/auth/realms/" + realmName + "/protocol/openid-connect/token");
final HttpClientConnection clientConnection = new HttpClientConnection(url, RequestMethod.POST);
clientConnection.addRequestHeader(CONTENT_TYPE_HEADER, FORM_MIME_TYPE);
return clientConnection;
}
private HttpClientConnection createServerProxyConnection(final String realmName, final String appName) throws URISyntaxException, IOException {
final URI url = new URI(authEndpoint);
final HttpClientConnection clientConnection = new HttpClientConnection(url, RequestMethod.POST);
clientConnection.addRequestHeader(CONTENT_TYPE_HEADER, TEXT_MIME_TYPE);
if (realmName != null && !realmName.isEmpty()) {
clientConnection.addRequestHeader(REALM_NAME_HEADER, realmName);
}
if (appName != null && !appName.isEmpty()) {
clientConnection.addRequestHeader(APPLICATION_NAME_HEADER, appName);
}
return clientConnection;
}
private KeycloakOpenidConnectResult receiveTokenByLogin(final String user, final String password, final String realmName, final String appName) throws IOException, URISyntaxException {
if (directConnect) {
final String content = "client_id=" + defaultAppName + "&username=" + user + "&password=" + password + "&grant_type=password";
return receiveToken(createDirectConnection(realmName), content);
} else {
final String content = "username=" + user + "&password=" + password + "&grant_type=password";
return receiveToken(createServerProxyConnection(realmName, appName), content);
}
}
private KeycloakOpenidConnectResult receiveTokenByRefresh(final String refreshToken, final String realmName, final String appName) throws IOException, URISyntaxException {
if (directConnect) {
final String content = "grant_type=refresh_token&refresh_token=" + refreshToken + "&client_id=" + defaultAppName;
return receiveToken(createDirectConnection(realmName), content);
} else {
final String content = "grant_type=refresh_token&refresh_token=" + refreshToken;
return receiveToken(createServerProxyConnection(realmName, appName), content);
}
}
private KeycloakOpenidConnectResult receiveToken(final HttpClientConnection connection, final String content) throws IOException {
Assert.requireNonNull(content, "content");
LOG.debug("receiving new token from keycloak server");
connection.setDoOutput(true);
connection.writeRequestContent(content);
final int responseCode = connection.readResponseCode();
if (responseCode == SC_HTTP_UNAUTHORIZED) {
throw new DolphinRuntimeException("Invalid login!");
}
final String input = connection.readUTFResponseContent();
final Gson gson = PlatformClient.getService(Gson.class);
final KeycloakOpenidConnectResult result = gson.fromJson(input, KeycloakOpenidConnectResult.class);
return result;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy