/* * Copyright 2016 Red Hat, Inc. and/or its affiliates. *   * 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.uberfire.ext.security.management.keycloak.client.auth.credentials; import java.io.IOException; import java.io.InputStream; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.jboss.resteasy.client.ClientResponse; import org.jboss.resteasy.client.ProxyFactory; import org.jboss.resteasy.client.core.BaseClientResponse; import org.jboss.resteasy.client.core.ClientErrorInterceptor; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.util.CaseInsensitiveMap; import org.keycloak.OAuth2Constants; import org.keycloak.common.util.Time; import org.keycloak.util.BasicAuthHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.uberfire.ext.security.management.keycloak.client.auth.TokenManager; import org.uberfire.ext.security.management.keycloak.client.auth.TokenService; /** * Token manager that uses credentials based authentication settings to manage the access token. * Handles: * - Public / non public clients * - Token refreshments based on OAuth2 token's expiration time. * @since 0.9.0 */ public class AuthTokenManager implements TokenManager { private static final Logger LOG = LoggerFactory.getLogger(AuthTokenManager.class); private static final long DEFAULT_MIN_VALIDITY = 30; private final AuthSettings config; long expirationTime; long minTokenValidity = DEFAULT_MIN_VALIDITY; AccessTokenResponse accessTokenResponse; private final ClientErrorInterceptor clientErrorInterceptor = new ClientErrorInterceptor() { @Override public void handle(ClientResponse<?> response) throws RuntimeException { // Whatever the error is, let's nullify the current access token response. AuthTokenManager.this.accessTokenResponse = null; // Handle some of the common errors. String error = null; Exception exception = null; try { BaseClientResponse r = (BaseClientResponse) response; InputStream stream = r.getStreamFactory().getInputStream(); stream.reset(); if (Response.Status.FORBIDDEN.equals(response.getResponseStatus())) { error = "Error handling the Keycloak token, status is FORBIDDEN"; } else if (Response.Status.UNAUTHORIZED.equals(response.getResponseStatus())) { error = "Error handling the Keycloak token, status is UNAUTHORIZED"; } else if (Response.Status.BAD_REQUEST.equals(response.getResponseStatus())) { error = "Error handling the Keycloak token, status is BAD_REQUEST. Response data: " + getResponseData(r); } else if (Response.Status.NOT_FOUND.equals(response.getResponseStatus())) { error = "Error handling the Keycloak token, status is NOT_FOUND."; } else if (!Response.Status.OK.equals(response.getResponseStatus())) { error = "Error handling the Keycloak token. Response status is " + response.getResponseStatus() + ". Response data: " + getResponseData(r); } } catch (IOException e) { error = "Error handling the Keycloak token."; exception = e; } finally { response.releaseConnection(); } // If error is handled here, log it and throw the exception. // Otherwise, let's Resteasy do the generic work after a client error. if (null != error) { LOG.error(error); if (null != exception) { throw new RuntimeException(error, exception); } else { throw new RuntimeException(error); } } } private String getResponseData(BaseClientResponse response) { try { return (String) response.getEntity(String.class); } catch (Exception e) { LOG.error("Error trying to obtain response data as String.", e); } return null; } }; public AuthTokenManager(AuthSettings config) { this.config = config; } @Override public void grantToken() { MultivaluedMap<String, String> mvm = new CaseInsensitiveMap<String>(); mvm.putSingle(OAuth2Constants.GRANT_TYPE, "password"); mvm.putSingle("username", config.getUsername()); mvm.putSingle("password", config.getPassword()); consumeGrantTokenService(mvm); } private void refreshToken() { MultivaluedMap<String, String> mvm = new CaseInsensitiveMap<String>(); mvm.putSingle(OAuth2Constants.GRANT_TYPE, "refresh_token"); mvm.putSingle("refresh_token", accessTokenResponse.getRefreshToken()); consumeGrantTokenService(mvm); } protected void consumeGrantTokenService(final MultivaluedMap<String, String> mvm) { boolean isPublic = config.isPublicClient(); String authorization = ""; if (isPublic) { // if client is public access type mvm.putSingle(OAuth2Constants.CLIENT_ID, config.getClientId()); } else { authorization = BasicAuthHelper.createHeader(config.getClientId(), config.getClientSecret()); } TokenService client = createTokenService(); AccessTokenResponse response = client.grantToken(config.getRealm(), authorization, mvm); int requestTime = Time.currentTime(); expirationTime = requestTime + response.getExpiresIn(); this.accessTokenResponse = response; } @Override public String getAccessTokenString() { if (null == this.accessTokenResponse) { grantToken(); } else if (tokenExpired()) { refreshToken(); } return accessTokenResponse != null ? accessTokenResponse.getToken() : null; } @Override public String getRealm() { return config.getRealm(); } TokenService createTokenService() { ResteasyProviderFactory pf = ResteasyProviderFactory.getInstance(); pf.addClientErrorInterceptor(clientErrorInterceptor); return ProxyFactory.create(TokenService.class, config.getServerUrl()); } private boolean tokenExpired() { return accessTokenResponse != null && (Time.currentTime() + minTokenValidity) >= expirationTime; } }