From 6380dc3d1bfa5ada93a2cd7834e5a29b9f2dc98a Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 17 Dec 2013 12:07:02 -0500 Subject: [PATCH] refactor out resteasy from adapters --- .../org/keycloak/AbstractOAuthClient.java | 155 ---- .../org/keycloak/AbstractOAuthClient.java | 94 +++ .../java/org/keycloak/SkeletonKeySession.java | 14 + .../main/java/org/keycloak/util/Encode.java | 526 +++++++++++++ .../org/keycloak/util/KeycloakUriBuilder.java | 717 ++++++++++++++++++ .../java/org/keycloak/util/PathHelper.java | 62 ++ examples/as7-eap-demo/customer-app/pom.xml | 20 - .../example/oauth/CustomerDatabaseClient.java | 36 - .../example/CustomerDatabaseClient.java | 49 ++ .../WEB-INF/jboss-deployment-structure.xml | 3 - .../src/main/webapp/WEB-INF/jboss-web.xml | 2 +- .../src/main/webapp/admin/admin.jsp | 2 +- .../src/main/webapp/customers/view.jsp | 12 +- .../as7-eap-demo/database-service/pom.xml | 25 +- .../WEB-INF/jboss-deployment-structure.xml | 1 - examples/as7-eap-demo/product-app/pom.xml | 20 - .../example/oauth/ProductDatabaseClient.java | 36 - .../example/oauth/ProductDatabaseClient.java | 49 ++ .../src/main/webapp/WEB-INF/jboss-web.xml | 2 +- .../src/main/webapp/products/view.jsp | 10 +- examples/as7-eap-demo/third-party/pom.xml | 12 +- .../example/oauth/Bootstrap.java | 4 +- .../example/oauth/ProductDatabaseClient.java | 59 +- .../WEB-INF/jboss-deployment-structure.xml | 2 - .../src/main/webapp/WEB-INF/web.xml | 2 +- .../third-party/src/main/webapp/pull_data.jsp | 3 +- .../third-party/src/main/webapp/redirect.jsp | 4 +- integration/adapter-core/pom.xml | 13 +- .../keycloak/adapters/HttpClientBuilder.java | 274 +++++++ .../keycloak/adapters/TokenGrantRequest.java | 140 ++++ .../adapters/config/AdapterConfig.java | 2 +- .../{ => config}/RealmConfiguration.java | 78 +- .../config}/RealmConfigurationLoader.java | 44 +- integration/as7-eap6/adapter/pom.xml | 40 +- .../as7/AuthenticatedActionsValve.java | 16 +- .../as7/BearerTokenAuthenticatorValve.java | 4 +- .../as7/CatalinaBearerTokenAuthenticator.java | 3 +- ...alve.java => OAuthAuthenticatorValve.java} | 15 +- .../adapters/as7/ServletOAuthLogin.java | 59 +- .../config/CatalinaAdapterConfigLoader.java | 4 +- .../as7/config/RealmConfigurationLoader.java | 97 --- integration/jaxrs-oauth-client/pom.xml | 71 ++ .../jaxrs/JaxrsBearerTokenFilter.java | 0 .../org/keycloak/jaxrs/JaxrsOAuthClient.java | 56 ++ integration/pom.xml | 2 + integration/servlet-oauth-client/pom.xml | 67 ++ .../keycloak/servlet/ServletOAuthClient.java | 70 +- integration/undertow/pom.xml | 33 +- .../KeycloakAuthenticationMechanism.java | 2 +- .../undertow/KeycloakServletExtension.java | 1 + .../adapters/undertow/OAuthAuthenticator.java | 54 +- ...ervletKeycloakAuthenticationMechanism.java | 2 +- .../undertow/ServletOAuthAuthenticator.java | 2 +- pom.xml | 11 +- services/pom.xml | 16 +- .../testsuite/DummySocialServlet.java | 9 +- .../org/keycloak/testsuite/OAuthClient.java | 8 +- 57 files changed, 2444 insertions(+), 670 deletions(-) delete mode 100755 core-jaxrs/src/main/java/org/keycloak/AbstractOAuthClient.java create mode 100755 core/src/main/java/org/keycloak/AbstractOAuthClient.java create mode 100755 core/src/main/java/org/keycloak/util/Encode.java create mode 100755 core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java create mode 100755 core/src/main/java/org/keycloak/util/PathHelper.java delete mode 100755 examples/as7-eap-demo/customer-app/src/main/java/org/jboss/resteasy/example/oauth/CustomerDatabaseClient.java create mode 100755 examples/as7-eap-demo/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java mode change 100644 => 100755 examples/as7-eap-demo/customer-app/src/main/webapp/admin/admin.jsp delete mode 100755 examples/as7-eap-demo/product-app/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java create mode 100755 examples/as7-eap-demo/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java rename examples/as7-eap-demo/third-party/src/main/java/org/{jboss/resteasy => keycloak}/example/oauth/Bootstrap.java (93%) rename examples/as7-eap-demo/third-party/src/main/java/org/{jboss/resteasy => keycloak}/example/oauth/ProductDatabaseClient.java (54%) mode change 100644 => 100755 examples/as7-eap-demo/third-party/src/main/webapp/pull_data.jsp mode change 100644 => 100755 examples/as7-eap-demo/third-party/src/main/webapp/redirect.jsp create mode 100755 integration/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java create mode 100755 integration/adapter-core/src/main/java/org/keycloak/adapters/TokenGrantRequest.java rename integration/adapter-core/src/main/java/org/keycloak/adapters/{ => config}/RealmConfiguration.java (55%) rename integration/{undertow/src/main/java/org/keycloak/adapters/undertow => adapter-core/src/main/java/org/keycloak/adapters/config}/RealmConfigurationLoader.java (51%) rename integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/{OAuthManagedResourceValve.java => OAuthAuthenticatorValve.java} (92%) delete mode 100755 integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/RealmConfigurationLoader.java create mode 100755 integration/jaxrs-oauth-client/pom.xml rename {core-jaxrs => integration/jaxrs-oauth-client}/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java (100%) rename {core-jaxrs => integration/jaxrs-oauth-client}/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java (60%) create mode 100755 integration/servlet-oauth-client/pom.xml rename {core-jaxrs => integration/servlet-oauth-client}/src/main/java/org/keycloak/servlet/ServletOAuthClient.java (61%) diff --git a/core-jaxrs/src/main/java/org/keycloak/AbstractOAuthClient.java b/core-jaxrs/src/main/java/org/keycloak/AbstractOAuthClient.java deleted file mode 100755 index b5975d6da3..0000000000 --- a/core-jaxrs/src/main/java/org/keycloak/AbstractOAuthClient.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.keycloak; - -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; -import org.jboss.resteasy.util.BasicAuthHelper; -import org.keycloak.representations.AccessTokenResponse; - -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import java.security.KeyStore; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AbstractOAuthClient { - public static final String OAUTH_TOKEN_REQUEST_STATE = "OAuth_Token_Request_State"; - protected String clientId; - protected String password; - protected KeyStore truststore; - protected String authUrl; - protected String codeUrl; - protected String stateCookieName = OAUTH_TOKEN_REQUEST_STATE; - protected String stateCookiePath; - protected Client client; - protected boolean isSecure; - protected final AtomicLong counter = new AtomicLong(); - - protected String getStateCode() { - return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); - } - - /** - * Creates a Client for obtaining access token from code - */ - public void start() { - if (client == null) { - client = new ResteasyClientBuilder().trustStore(truststore) - .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY) - .connectionPoolSize(10) - .build(); - } - } - - /** - * closes cllient - */ - public void stop() { - client.close(); - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public KeyStore getTruststore() { - return truststore; - } - - public void setTruststore(KeyStore truststore) { - this.truststore = truststore; - } - - public String getAuthUrl() { - return authUrl; - } - - - - public void setAuthUrl(String authUrl) { - this.authUrl = authUrl; - } - - public String getCodeUrl() { - return codeUrl; - } - - public void setCodeUrl(String codeUrl) { - this.codeUrl = codeUrl; - } - - public String getStateCookieName() { - return stateCookieName; - } - - public void setStateCookieName(String stateCookieName) { - this.stateCookieName = stateCookieName; - } - - public String getStateCookiePath() { - return stateCookiePath; - } - - public void setStateCookiePath(String stateCookiePath) { - this.stateCookiePath = stateCookiePath; - } - - public Client getClient() { - return client; - } - - public void setClient(Client client) { - this.client = client; - } - - public String resolveBearerToken(String redirectUri, String code) { - redirectUri = stripOauthParametersFromRedirect(redirectUri); - String authHeader = BasicAuthHelper.createHeader(clientId, password); - Form codeForm = new Form() - .param("grant_type", "authorization_code") - .param("code", code) - .param("client_id", clientId) - .param("password", password) - .param("redirect_uri", redirectUri); - Response res = client.target(codeUrl).request().header(HttpHeaders.AUTHORIZATION, authHeader).post(Entity.form(codeForm)); - try { - if (res.getStatus() == 400) { - throw new BadRequestException(); - } else if (res.getStatus() != 200) { - throw new InternalServerErrorException(new Exception("Unknown error when getting acess token")); - } - AccessTokenResponse tokenResponse = res.readEntity(AccessTokenResponse.class); - return tokenResponse.getToken(); - } finally { - res.close(); - } - } - - protected String stripOauthParametersFromRedirect(String uri) { - UriBuilder builder = UriBuilder.fromUri(uri) - .replaceQueryParam("code", null) - .replaceQueryParam("state", null); - return builder.build().toString(); - } - -} diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java new file mode 100755 index 0000000000..9b15f7758a --- /dev/null +++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java @@ -0,0 +1,94 @@ +package org.keycloak; + +import org.keycloak.util.KeycloakUriBuilder; + +import java.security.KeyStore; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AbstractOAuthClient { + public static final String OAUTH_TOKEN_REQUEST_STATE = "OAuth_Token_Request_State"; + protected String clientId; + protected String password; + protected KeyStore truststore; + protected String authUrl; + protected String codeUrl; + protected String stateCookieName = OAUTH_TOKEN_REQUEST_STATE; + protected String stateCookiePath; + protected boolean isSecure; + protected final AtomicLong counter = new AtomicLong(); + + protected String getStateCode() { + return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public KeyStore getTruststore() { + return truststore; + } + + public void setTruststore(KeyStore truststore) { + this.truststore = truststore; + } + + public String getAuthUrl() { + return authUrl; + } + + + + public void setAuthUrl(String authUrl) { + this.authUrl = authUrl; + } + + public String getCodeUrl() { + return codeUrl; + } + + public void setCodeUrl(String codeUrl) { + this.codeUrl = codeUrl; + } + + public String getStateCookieName() { + return stateCookieName; + } + + public void setStateCookieName(String stateCookieName) { + this.stateCookieName = stateCookieName; + } + + public String getStateCookiePath() { + return stateCookiePath; + } + + public void setStateCookiePath(String stateCookiePath) { + this.stateCookiePath = stateCookiePath; + } + + protected String stripOauthParametersFromRedirect(String uri) { + KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(uri) + .replaceQueryParam("code", null) + .replaceQueryParam("state", null); + return builder.build().toString(); + } + +} diff --git a/core/src/main/java/org/keycloak/SkeletonKeySession.java b/core/src/main/java/org/keycloak/SkeletonKeySession.java index 3f10c7e896..68592839f6 100755 --- a/core/src/main/java/org/keycloak/SkeletonKeySession.java +++ b/core/src/main/java/org/keycloak/SkeletonKeySession.java @@ -34,4 +34,18 @@ public class SkeletonKeySession implements Serializable { return metadata; } + protected static ThreadLocal local = new ThreadLocal(); + + public static void pushContext(SkeletonKeySession session) { + local.set(session); + } + + public static void clearContext() { + local.set(null); + } + + public static SkeletonKeySession getContext() { + return local.get(); + } + } diff --git a/core/src/main/java/org/keycloak/util/Encode.java b/core/src/main/java/org/keycloak/util/Encode.java new file mode 100755 index 0000000000..341f0dfc0c --- /dev/null +++ b/core/src/main/java/org/keycloak/util/Encode.java @@ -0,0 +1,526 @@ +package org.keycloak.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class Encode +{ + private static final String UTF_8 = "UTF-8"; + + private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter"); + + private static final String[] pathEncoding = new String[128]; + private static final String[] pathSegmentEncoding = new String[128]; + private static final String[] matrixParameterEncoding = new String[128]; + private static final String[] queryNameValueEncoding = new String[128]; + private static final String[] queryStringEncoding = new String[128]; + + static + { + /* + * Encode via RFC 3986. PCHAR is allowed allong with '/' + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * + */ + for (int i = 0; i < 128; i++) + { + if (i >= 'a' && i <= 'z') continue; + if (i >= 'A' && i <= 'Z') continue; + if (i >= '0' && i <= '9') continue; + switch ((char) i) + { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case '/': + case ';': + case '=': + case ':': + case '@': + continue; + } + StringBuffer sb = new StringBuffer(); + sb.append((char) i); + pathEncoding[i] = URLEncoder.encode(sb.toString()); + } + pathEncoding[' '] = "%20"; + System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length); + matrixParameterEncoding[';'] = "%3B"; + matrixParameterEncoding['='] = "%3D"; + matrixParameterEncoding['/'] = "%2F"; // RESTEASY-729 + System.arraycopy(pathEncoding, 0, pathSegmentEncoding, 0, pathEncoding.length); + pathSegmentEncoding['/'] = "%2F"; + /* + * Encode via RFC 3986. + * + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * space encoded as '+' + * + */ + for (int i = 0; i < 128; i++) + { + if (i >= 'a' && i <= 'z') continue; + if (i >= 'A' && i <= 'Z') continue; + if (i >= '0' && i <= '9') continue; + switch ((char) i) + { + case '-': + case '.': + case '_': + case '~': + case '?': + continue; + case ' ': + queryNameValueEncoding[i] = "+"; + continue; + } + StringBuffer sb = new StringBuffer(); + sb.append((char) i); + queryNameValueEncoding[i] = URLEncoder.encode(sb.toString()); + } + + /* + * query = *( pchar / "/" / "?" ) + + */ + for (int i = 0; i < 128; i++) + { + if (i >= 'a' && i <= 'z') continue; + if (i >= 'A' && i <= 'Z') continue; + if (i >= '0' && i <= '9') continue; + switch ((char) i) + { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + case ':': + case '@': + case '?': + case '/': + continue; + case ' ': + queryStringEncoding[i] = "%20"; + continue; + } + StringBuffer sb = new StringBuffer(); + sb.append((char) i); + queryStringEncoding[i] = URLEncoder.encode(sb.toString()); + } + } + + /** + * Keep encoded values "%..." and template parameters intact. + */ + public static String encodeQueryString(String value) + { + return encodeValue(value, queryStringEncoding); + } + + /** + * Keep encoded values "%...", matrix parameters, template parameters, and '/' characters intact. + */ + public static String encodePath(String value) + { + return encodeValue(value, pathEncoding); + } + + /** + * Keep encoded values "%...", matrix parameters and template parameters intact. + */ + public static String encodePathSegment(String value) + { + return encodeValue(value, pathSegmentEncoding); + } + + /** + * Keep encoded values "%..." and template parameters intact. + */ + public static String encodeFragment(String value) + { + return encodeValue(value, queryNameValueEncoding); + } + + /** + * Keep encoded values "%..." and template parameters intact. + */ + public static String encodeMatrixParam(String value) + { + return encodeValue(value, matrixParameterEncoding); + } + + /** + * Keep encoded values "%..." and template parameters intact. + */ + public static String encodeQueryParam(String value) + { + return encodeValue(value, queryNameValueEncoding); + } + + //private static final Pattern nonCodes = Pattern.compile("%([^a-fA-F0-9]|$)"); + private static final Pattern nonCodes = Pattern.compile("%([^a-fA-F0-9]|[a-fA-F0-9]$|$|[a-fA-F0-9][^a-fA-F0-9])"); + private static final Pattern encodedChars = Pattern.compile("%([a-fA-F0-9][a-fA-F0-9])"); + private static final Pattern encodedCharsMulti = Pattern.compile("((%[a-fA-F0-9][a-fA-F0-9])+)"); + + public static String decodePath(String path) + { + Matcher matcher = encodedCharsMulti.matcher(path); + StringBuffer buf = new StringBuffer(); + CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder(); + while (matcher.find()) + { + decoder.reset(); + String decoded = decodeBytes(matcher.group(1), decoder); + decoded = decoded.replace("\\", "\\\\"); + decoded = decoded.replace("$", "\\$"); + matcher.appendReplacement(buf, decoded); + } + matcher.appendTail(buf); + return buf.toString(); + } + + private static String decodeBytes(String enc, CharsetDecoder decoder) + { + Matcher matcher = encodedChars.matcher(enc); + StringBuffer buf = new StringBuffer(); + ByteBuffer bytes = ByteBuffer.allocate(enc.length() / 3); + while (matcher.find()) + { + int b = Integer.parseInt(matcher.group(1), 16); + bytes.put((byte) b); + } + bytes.flip(); + try + { + return decoder.decode(bytes).toString(); + } + catch (CharacterCodingException e) + { + throw new RuntimeException(e); + } + } + + /** + * Encode '%' if it is not an encoding sequence + * + * @param string + * @return + */ + public static String encodeNonCodes(String string) + { + Matcher matcher = nonCodes.matcher(string); + StringBuffer buf = new StringBuffer(); + + + // FYI: we do not use the no-arg matcher.find() + // coupled with matcher.appendReplacement() + // because the matched text may contain + // a second % and we must make sure we + // encode it (if necessary). + int idx = 0; + while (matcher.find(idx)) + { + int start = matcher.start(); + buf.append(string.substring(idx, start)); + buf.append("%25"); + idx = start + 1; + } + buf.append(string.substring(idx)); + return buf.toString(); + } + + public static boolean savePathParams(String segment, StringBuffer newSegment, List params) + { + boolean foundParam = false; + // Regular expressions can have '{' and '}' characters. Replace them to do match + segment = PathHelper.replaceEnclosedCurlyBraces(segment); + Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment); + while (matcher.find()) + { + foundParam = true; + String group = matcher.group(); + // Regular expressions can have '{' and '}' characters. Recover earlier replacement + params.add(PathHelper.recoverEnclosedCurlyBraces(group)); + matcher.appendReplacement(newSegment, "_resteasy_uri_parameter"); + } + matcher.appendTail(newSegment); + return foundParam; + } + + /** + * Keep encoded values "%..." and template parameters intact i.e. "{x}" + * + * @param segment + * @param encoding + * @return + */ + public static String encodeValue(String segment, String[] encoding) + { + ArrayList params = new ArrayList(); + boolean foundParam = false; + StringBuffer newSegment = new StringBuffer(); + if (savePathParams(segment, newSegment, params)) + { + foundParam = true; + segment = newSegment.toString(); + } + String result = encodeFromArray(segment, encoding, false); + result = encodeNonCodes(result); + segment = result; + if (foundParam) + { + segment = pathParamReplacement(segment, params); + } + return segment; + } + + /** + * Encode via RFC 3986. PCHAR is allowed allong with '/' + *

+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ + public static String encodePathAsIs(String segment) + { + return encodeFromArray(segment, pathEncoding, true); + } + + /** + * Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p" + * + * @param segment + * @return + */ + public static String encodePathSaveEncodings(String segment) + { + String result = encodeFromArray(segment, pathEncoding, false); + result = encodeNonCodes(result); + return result; + } + + /** + * Encode via RFC 3986. PCHAR is allowed allong with '/' + *

+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + */ + public static String encodePathSegmentAsIs(String segment) + { + return encodeFromArray(segment, pathSegmentEncoding, true); + } + + /** + * Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p" + * + * @param segment + * @return + */ + public static String encodePathSegmentSaveEncodings(String segment) + { + String result = encodeFromArray(segment, pathSegmentEncoding, false); + result = encodeNonCodes(result); + return result; + } + + + /** + * Encodes everything of a query parameter name or value. + * + * @param nameOrValue + * @return + */ + public static String encodeQueryParamAsIs(String nameOrValue) + { + return encodeFromArray(nameOrValue, queryNameValueEncoding, true); + } + + /** + * Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p" + * + * @param segment + * @return + */ + public static String encodeQueryParamSaveEncodings(String segment) + { + String result = encodeFromArray(segment, queryNameValueEncoding, false); + result = encodeNonCodes(result); + return result; + } + + public static String encodeFragmentAsIs(String nameOrValue) + { + return encodeFromArray(nameOrValue, queryNameValueEncoding, true); + } + + protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent) + { + StringBuffer result = new StringBuffer(); + for (int i = 0; i < segment.length(); i++) + { + if (!encodePercent && segment.charAt(i) == '%') + { + result.append(segment.charAt(i)); + continue; + } + int idx = segment.charAt(i); + String encoding = encode(idx, encodingMap); + if (encoding == null) + { + result.append(segment.charAt(i)); + } + else + { + result.append(encoding); + } + } + return result.toString(); + } + + /** + * @param zhar integer representation of character + * @param encodingMap encoding map + * @return URL encoded character + */ + private static String encode(int zhar, String[] encodingMap) + { + String encoded; + if (zhar < encodingMap.length) + { + encoded = encodingMap[zhar]; + } + else + { + try + { + encoded = URLEncoder.encode(Character.toString((char) zhar), UTF_8); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + return encoded; + } + + public static String pathParamReplacement(String segment, List params) + { + StringBuffer newSegment = new StringBuffer(); + Matcher matcher = PARAM_REPLACEMENT.matcher(segment); + int i = 0; + while (matcher.find()) + { + String replacement = params.get(i++); + // double encode slashes, so that slashes stay where they are + replacement = replacement.replace("\\", "\\\\"); + replacement = replacement.replace("$", "\\$"); + matcher.appendReplacement(newSegment, replacement); + } + matcher.appendTail(newSegment); + segment = newSegment.toString(); + return segment; + } + + /** + * decode an encoded map + * + * @param map + * @return + */ + public static MultivaluedHashMap decode(MultivaluedHashMap map) + { + MultivaluedHashMap decoded = new MultivaluedHashMap(); + for (Map.Entry> entry : map.entrySet()) + { + List values = entry.getValue(); + for (String value : values) + { + try + { + decoded.add(URLDecoder.decode(entry.getKey(), UTF_8), URLDecoder.decode(value, UTF_8)); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + } + return decoded; + } + + public static MultivaluedHashMap encode(MultivaluedHashMap map) + { + MultivaluedHashMap decoded = new MultivaluedHashMap(); + for (Map.Entry> entry : map.entrySet()) + { + List values = entry.getValue(); + for (String value : values) + { + try + { + decoded.add(URLEncoder.encode(entry.getKey(), UTF_8), URLEncoder.encode(value, UTF_8)); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + } + return decoded; + } + + public static String decode(String string) + { + try + { + return URLDecoder.decode(string, UTF_8); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + } + +} diff --git a/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java b/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java new file mode 100755 index 0000000000..111c4a4095 --- /dev/null +++ b/core/src/main/java/org/keycloak/util/KeycloakUriBuilder.java @@ -0,0 +1,717 @@ +package org.keycloak.util; + + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakUriBuilder { + + private String host; + private String scheme; + private int port = -1; + + private String userInfo; + private String path; + private String query; + private String fragment; + private String ssp; + private String authority; + + public static KeycloakUriBuilder fromUri(URI uri) { + return new KeycloakUriBuilder().uri(uri); + } + + public static KeycloakUriBuilder fromUri(String uriTemplate) { + return new KeycloakUriBuilder().uri(uriTemplate); + } + + public static KeycloakUriBuilder fromPath(String path) throws IllegalArgumentException { + return new KeycloakUriBuilder().path(path); + } + + + public KeycloakUriBuilder clone() { + KeycloakUriBuilder impl = new KeycloakUriBuilder(); + impl.host = host; + impl.scheme = scheme; + impl.port = port; + impl.userInfo = userInfo; + impl.path = path; + impl.query = query; + impl.fragment = fragment; + impl.ssp = ssp; + impl.authority = authority; + + return impl; + } + + public static final Pattern opaqueUri = Pattern.compile("^([^:/?#]+):([^/].*)"); + public static final Pattern hierarchicalUri = Pattern.compile("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"); + private static final Pattern hostPortPattern = Pattern.compile("([^/:]+):(\\d+)"); + + public static boolean compare(String s1, String s2) { + if (s1 == s2) return true; + if (s1 == null || s2 == null) return false; + return s1.equals(s2); + } + + public static URI relativize(URI from, URI to) { + if (!compare(from.getScheme(), to.getScheme())) return to; + if (!compare(from.getHost(), to.getHost())) return to; + if (from.getPort() != to.getPort()) return to; + if (from.getPath() == null && to.getPath() == null) return URI.create(""); + else if (from.getPath() == null) return URI.create(to.getPath()); + else if (to.getPath() == null) return to; + + + String fromPath = from.getPath(); + if (fromPath.startsWith("/")) fromPath = fromPath.substring(1); + String[] fsplit = fromPath.split("/"); + String toPath = to.getPath(); + if (toPath.startsWith("/")) toPath = toPath.substring(1); + String[] tsplit = toPath.split("/"); + + int f = 0; + + for (; f < fsplit.length && f < tsplit.length; f++) { + if (!fsplit[f].equals(tsplit[f])) break; + } + + KeycloakUriBuilder builder = KeycloakUriBuilder.fromPath(""); + for (int i = f; i < fsplit.length; i++) builder.path(".."); + for (int i = f; i < tsplit.length; i++) builder.path(tsplit[i]); + return builder.build(); + } + + /** + * You may put path parameters anywhere within the uriTemplate except port + * + * @param uriTemplate + * @return + */ + public static KeycloakUriBuilder fromTemplate(String uriTemplate) { + KeycloakUriBuilder impl = new KeycloakUriBuilder(); + impl.uriTemplate(uriTemplate); + return impl; + } + + /** + * You may put path parameters anywhere within the uriTemplate except port + * + * @param uriTemplate + * @return + */ + public KeycloakUriBuilder uriTemplate(String uriTemplate) { + if (uriTemplate == null) throw new IllegalArgumentException("uriTemplate parameter is null"); + Matcher opaque = opaqueUri.matcher(uriTemplate); + if (opaque.matches()) { + this.authority = null; + this.host = null; + this.port = -1; + this.userInfo = null; + this.query = null; + this.scheme = opaque.group(1); + this.ssp = opaque.group(2); + return this; + } else { + Matcher match = hierarchicalUri.matcher(uriTemplate); + if (match.matches()) { + ssp = null; + return parseHierarchicalUri(uriTemplate, match); + } + } + throw new IllegalArgumentException("Illegal uri template: " + uriTemplate); + } + + protected KeycloakUriBuilder parseHierarchicalUri(String uriTemplate, Matcher match) { + boolean scheme = match.group(2) != null; + if (scheme) this.scheme = match.group(2); + String authority = match.group(4); + if (authority != null) { + this.authority = null; + String host = match.group(4); + int at = host.indexOf('@'); + if (at > -1) { + String user = host.substring(0, at); + host = host.substring(at + 1); + this.userInfo = user; + } + Matcher hostPortMatch = hostPortPattern.matcher(host); + if (hostPortMatch.matches()) { + this.host = hostPortMatch.group(1); + int val = 0; + try { + this.port = Integer.parseInt(hostPortMatch.group(2)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Illegal uri template: " + uriTemplate, e); + } + } else { + this.host = host; + } + } + if (match.group(5) != null) { + String group = match.group(5); + if (!scheme && !"".equals(group) && !group.startsWith("/") && group.indexOf(':') > -1) + throw new IllegalArgumentException("Illegal uri template: " + uriTemplate); + if (!"".equals(group)) replacePath(group); + } + if (match.group(7) != null) replaceQuery(match.group(7)); + if (match.group(9) != null) fragment(match.group(9)); + return this; + } + + public KeycloakUriBuilder uri(String uriTemplate) throws IllegalArgumentException { + return uriTemplate(uriTemplate); + } + + public KeycloakUriBuilder uri(URI uri) throws IllegalArgumentException { + if (uri == null) throw new IllegalArgumentException("URI was null"); + + if (uri.getRawFragment() != null) fragment = uri.getRawFragment(); + + if (uri.isOpaque()) { + scheme = uri.getScheme(); + ssp = uri.getRawSchemeSpecificPart(); + return this; + } + + if (uri.getScheme() == null) { + if (ssp != null) { + if (uri.getRawSchemeSpecificPart() != null) { + ssp = uri.getRawSchemeSpecificPart(); + return this; + } + } + } else { + scheme = uri.getScheme(); + } + + ssp = null; + if (uri.getRawAuthority() != null) { + if (uri.getRawUserInfo() == null && uri.getHost() == null && uri.getPort() == -1) { + authority = uri.getRawAuthority(); + userInfo = null; + host = null; + port = -1; + } else { + authority = null; + if (uri.getRawUserInfo() != null) { + userInfo = uri.getRawUserInfo(); + } + if (uri.getHost() != null) { + host = uri.getHost(); + } + if (uri.getPort() != -1) { + port = uri.getPort(); + } + } + } + + if (uri.getRawPath() != null && uri.getRawPath().length() > 0) { + path = uri.getRawPath(); + } + if (uri.getRawQuery() != null && uri.getRawQuery().length() > 0) { + query = uri.getRawQuery(); + } + + return this; + } + + public KeycloakUriBuilder scheme(String scheme) throws IllegalArgumentException { + this.scheme = scheme; + return this; + } + + public KeycloakUriBuilder schemeSpecificPart(String ssp) throws IllegalArgumentException { + if (ssp == null) throw new IllegalArgumentException("schemeSpecificPart was null"); + + StringBuilder sb = new StringBuilder(); + if (scheme != null) sb.append(scheme).append(':'); + if (ssp != null) + sb.append(ssp); + if (fragment != null && fragment.length() > 0) sb.append('#').append(fragment); + URI uri = URI.create(sb.toString()); + + if (uri.getRawSchemeSpecificPart() != null && uri.getRawPath() == null) { + this.ssp = uri.getRawSchemeSpecificPart(); + } else { + this.ssp = null; + userInfo = uri.getRawUserInfo(); + host = uri.getHost(); + port = uri.getPort(); + path = uri.getRawPath(); + query = uri.getRawQuery(); + + } + return this; + + } + + public KeycloakUriBuilder userInfo(String ui) { + this.userInfo = ui; + return this; + } + + public KeycloakUriBuilder host(String host) throws IllegalArgumentException { + if (host != null && host.equals("")) throw new IllegalArgumentException("invalid host"); + this.host = host; + return this; + } + + public KeycloakUriBuilder port(int port) throws IllegalArgumentException { + if (port < -1) throw new IllegalArgumentException("Invalid port value"); + this.port = port; + return this; + } + + protected static String paths(boolean encode, String basePath, String... segments) { + String path = basePath; + if (path == null) path = ""; + for (String segment : segments) { + if ("".equals(segment)) continue; + if (path.endsWith("/")) { + if (segment.startsWith("/")) { + segment = segment.substring(1); + if ("".equals(segment)) continue; + } + if (encode) segment = Encode.encodePath(segment); + path += segment; + } else { + if (encode) segment = Encode.encodePath(segment); + if ("".equals(path)) { + path = segment; + } else if (segment.startsWith("/")) { + path += segment; + } else { + path += "/" + segment; + } + } + + } + return path; + } + + public KeycloakUriBuilder path(String segment) throws IllegalArgumentException { + if (segment == null) throw new IllegalArgumentException("path was null"); + path = paths(true, path, segment); + return this; + } + + public KeycloakUriBuilder replaceMatrix(String matrix) throws IllegalArgumentException { + if (matrix == null) matrix = ""; + if (!matrix.startsWith(";")) matrix = ";" + matrix; + matrix = Encode.encodePath(matrix); + if (path == null) { + path = matrix; + } else { + int start = path.lastIndexOf('/'); + if (start < 0) start = 0; + int matrixIndex = path.indexOf(';', start); + if (matrixIndex > -1) path = path.substring(0, matrixIndex) + matrix; + else path += matrix; + + } + return this; + } + + public KeycloakUriBuilder replaceQuery(String query) throws IllegalArgumentException { + if (query == null || query.length() == 0) { + this.query = null; + return this; + } + this.query = Encode.encodeQueryString(query); + return this; + } + + public KeycloakUriBuilder fragment(String fragment) throws IllegalArgumentException { + if (fragment == null) { + this.fragment = null; + return this; + } + this.fragment = Encode.encodeFragment(fragment); + return this; + } + + /** + * Only replace path params in path of URI. This changes state of URIBuilder. + * + * @param name + * @param value + * @param isEncoded + * @return + */ + public KeycloakUriBuilder substitutePathParam(String name, Object value, boolean isEncoded) { + if (path != null) { + StringBuffer buffer = new StringBuffer(); + replacePathParameter(name, value.toString(), isEncoded, path, buffer, false); + path = buffer.toString(); + } + return this; + } + + public URI buildFromMap(Map values) throws IllegalArgumentException { + if (values == null) throw new IllegalArgumentException("values parameter is null"); + return buildUriFromMap(values, false, true); + } + + public URI buildFromEncodedMap(Map values) throws IllegalArgumentException { + if (values == null) throw new IllegalArgumentException("values parameter is null"); + return buildUriFromMap(values, true, false); + } + + public URI buildFromMap(Map values, boolean encodeSlashInPath) throws IllegalArgumentException { + if (values == null) throw new IllegalArgumentException("values parameter is null"); + return buildUriFromMap(values, false, encodeSlashInPath); + } + + protected URI buildUriFromMap(Map paramMap, boolean fromEncodedMap, boolean encodeSlash) throws IllegalArgumentException { + String buf = buildString(paramMap, fromEncodedMap, false, encodeSlash); + try { + return URI.create(buf); + } catch (Exception e) { + throw new RuntimeException("Failed to create URI: " + buf, e); + } + } + + private String buildString(Map paramMap, boolean fromEncodedMap, boolean isTemplate, boolean encodeSlash) { + for (Map.Entry entry : paramMap.entrySet()) { + if (entry.getKey() == null) throw new IllegalArgumentException("map key is null"); + if (entry.getValue() == null) throw new IllegalArgumentException("map value is null"); + } + StringBuffer buffer = new StringBuffer(); + + if (scheme != null) + replaceParameter(paramMap, fromEncodedMap, isTemplate, scheme, buffer, encodeSlash).append(":"); + if (ssp != null) { + buffer.append(ssp); + } else if (userInfo != null || host != null || port != -1) { + buffer.append("//"); + if (userInfo != null) + replaceParameter(paramMap, fromEncodedMap, isTemplate, userInfo, buffer, encodeSlash).append("@"); + if (host != null) { + if ("".equals(host)) throw new RuntimeException("empty host name"); + replaceParameter(paramMap, fromEncodedMap, isTemplate, host, buffer, encodeSlash); + } + if (port != -1) buffer.append(":").append(Integer.toString(port)); + } else if (authority != null) { + buffer.append("//"); + replaceParameter(paramMap, fromEncodedMap, isTemplate, authority, buffer, encodeSlash); + } + if (path != null) { + StringBuffer tmp = new StringBuffer(); + replaceParameter(paramMap, fromEncodedMap, isTemplate, path, tmp, encodeSlash); + String tmpPath = tmp.toString(); + if (userInfo != null || host != null) { + if (!tmpPath.startsWith("/")) buffer.append("/"); + } + buffer.append(tmpPath); + } + if (query != null) { + buffer.append("?"); + replaceQueryStringParameter(paramMap, fromEncodedMap, isTemplate, query, buffer); + } + if (fragment != null) { + buffer.append("#"); + replaceParameter(paramMap, fromEncodedMap, isTemplate, fragment, buffer, encodeSlash); + } + return buffer.toString(); + } + + protected StringBuffer replacePathParameter(String name, String value, boolean isEncoded, String string, StringBuffer buffer, boolean encodeSlash) { + Matcher matcher = createUriParamMatcher(string); + while (matcher.find()) { + String param = matcher.group(1); + if (!param.equals(name)) continue; + if (!isEncoded) { + if (encodeSlash) value = Encode.encodePath(value); + else value = Encode.encodePathSegment(value); + + } else { + value = Encode.encodeNonCodes(value); + } + // if there is a $ then we must backslash it or it will screw up regex group substitution + value = value.replace("$", "\\$"); + matcher.appendReplacement(buffer, value); + } + matcher.appendTail(buffer); + return buffer; + } + + public static Matcher createUriParamMatcher(String string) { + Matcher matcher = PathHelper.URI_PARAM_PATTERN.matcher(PathHelper.replaceEnclosedCurlyBraces(string)); + return matcher; + } + + protected StringBuffer replaceParameter(Map paramMap, boolean fromEncodedMap, boolean isTemplate, String string, StringBuffer buffer, boolean encodeSlash) { + Matcher matcher = createUriParamMatcher(string); + while (matcher.find()) { + String param = matcher.group(1); + Object valObj = paramMap.get(param); + if (valObj == null && !isTemplate) { + throw new IllegalArgumentException("NULL value for template parameter: " + param); + } else if (valObj == null && isTemplate) { + matcher.appendReplacement(buffer, matcher.group()); + continue; + } + String value = valObj.toString(); + if (value != null) { + if (!fromEncodedMap) { + if (encodeSlash) value = Encode.encodePathSegmentAsIs(value); + else value = Encode.encodePathAsIs(value); + } else { + if (encodeSlash) value = Encode.encodePathSegmentSaveEncodings(value); + else value = Encode.encodePathSaveEncodings(value); + } + matcher.appendReplacement(buffer, Matcher.quoteReplacement(value)); + } else { + throw new IllegalArgumentException("path param " + param + " has not been provided by the parameter map"); + } + } + matcher.appendTail(buffer); + return buffer; + } + + protected StringBuffer replaceQueryStringParameter(Map paramMap, boolean fromEncodedMap, boolean isTemplate, String string, StringBuffer buffer) { + Matcher matcher = createUriParamMatcher(string); + while (matcher.find()) { + String param = matcher.group(1); + Object valObj = paramMap.get(param); + if (valObj == null && !isTemplate) { + throw new IllegalArgumentException("NULL value for template parameter: " + param); + } else if (valObj == null && isTemplate) { + matcher.appendReplacement(buffer, matcher.group()); + continue; + } + String value = valObj.toString(); + if (value != null) { + if (!fromEncodedMap) { + value = Encode.encodeQueryParamAsIs(value); + } else { + value = Encode.encodeQueryParamSaveEncodings(value); + } + matcher.appendReplacement(buffer, value); + } else { + throw new IllegalArgumentException("path param " + param + " has not been provided by the parameter map"); + } + } + matcher.appendTail(buffer); + return buffer; + } + + /** + * Return a unique order list of path params + * + * @return + */ + public List getPathParamNamesInDeclarationOrder() { + List params = new ArrayList(); + HashSet set = new HashSet(); + if (scheme != null) addToPathParamList(params, set, scheme); + if (userInfo != null) addToPathParamList(params, set, userInfo); + if (host != null) addToPathParamList(params, set, host); + if (path != null) addToPathParamList(params, set, path); + if (query != null) addToPathParamList(params, set, query); + if (fragment != null) addToPathParamList(params, set, fragment); + + return params; + } + + private void addToPathParamList(List params, HashSet set, String string) { + Matcher matcher = PathHelper.URI_PARAM_PATTERN.matcher(PathHelper.replaceEnclosedCurlyBraces(string)); + while (matcher.find()) { + String param = matcher.group(1); + if (set.contains(param)) continue; + else { + set.add(param); + params.add(param); + } + } + } + + public URI build(Object... values) throws IllegalArgumentException { + if (values == null) throw new IllegalArgumentException("values parameter is null"); + return buildFromValues(true, false, values); + } + + protected URI buildFromValues(boolean encodeSlash, boolean encoded, Object... values) { + List params = getPathParamNamesInDeclarationOrder(); + if (values.length < params.size()) + throw new IllegalArgumentException("You did not supply enough values to fill path parameters"); + + Map pathParams = new HashMap(); + + + for (int i = 0; i < params.size(); i++) { + String pathParam = params.get(i); + Object val = values[i]; + if (val == null) throw new IllegalArgumentException("A value was null"); + pathParams.put(pathParam, val.toString()); + } + String buf = null; + try { + buf = buildString(pathParams, encoded, false, encodeSlash); + return new URI(buf); + //return URI.create(buf); + } catch (Exception e) { + throw new RuntimeException("Failed to create URI: " + buf, e); + } + } + + public KeycloakUriBuilder matrixParam(String name, Object... values) throws IllegalArgumentException { + if (name == null) throw new IllegalArgumentException("name parameter is null"); + if (values == null) throw new IllegalArgumentException("values parameter is null"); + if (path == null) path = ""; + for (Object val : values) { + if (val == null) throw new IllegalArgumentException("null value"); + path += ";" + Encode.encodeMatrixParam(name) + "=" + Encode.encodeMatrixParam(val.toString()); + } + return this; + } + + private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter"); + + + public KeycloakUriBuilder queryParam(String name, Object... values) throws IllegalArgumentException { + if (name == null) throw new IllegalArgumentException("name parameter is null"); + if (values == null) throw new IllegalArgumentException("values parameter is null"); + for (Object value : values) { + if (value == null) throw new IllegalArgumentException("A passed in value was null"); + if (query == null) query = ""; + else query += "&"; + query += Encode.encodeQueryParam(name) + "=" + Encode.encodeQueryParam(value.toString()); + } + return this; + } + + public KeycloakUriBuilder replaceQueryParam(String name, Object... values) throws IllegalArgumentException { + if (name == null) throw new IllegalArgumentException("name parameter is null"); + if (query == null || query.equals("")) { + if (values != null) return queryParam(name, values); + return this; + } + + String[] params = query.split("&"); + query = null; + + String replacedName = Encode.encodeQueryParam(name); + + + for (String param : params) { + int pos = param.indexOf('='); + if (pos >= 0) { + String paramName = param.substring(0, pos); + if (paramName.equals(replacedName)) continue; + } else { + if (param.equals(replacedName)) continue; + } + if (query == null) query = ""; + else query += "&"; + query += param; + } + // don't set values if values is null + if (values == null) return this; + // don't set values if values is null + if (values == null) return this; + return queryParam(name, values); + } + + public String getHost() { + return host; + } + + public String getScheme() { + return scheme; + } + + public int getPort() { + return port; + } + + public String getUserInfo() { + return userInfo; + } + + public String getPath() { + return path; + } + + public String getQuery() { + return query; + } + + public String getFragment() { + return fragment; + } + + public KeycloakUriBuilder segment(String... segments) throws IllegalArgumentException { + if (segments == null) throw new IllegalArgumentException("segments parameter was null"); + for (String segment : segments) { + if (segment == null) throw new IllegalArgumentException("A segment is null"); + path(Encode.encodePathSegment(segment)); + } + return this; + } + + public KeycloakUriBuilder replacePath(String path) { + if (path == null) { + this.path = null; + return this; + } + this.path = Encode.encodePath(path); + return this; + } + + public URI build(Object[] values, boolean encodeSlashInPath) throws IllegalArgumentException { + if (values == null) throw new IllegalArgumentException("values param is null"); + return buildFromValues(encodeSlashInPath, false, values); + } + + public String toTemplate() { + return buildString(new HashMap(), true, true, true); + } + + public KeycloakUriBuilder resolveTemplate(String name, Object value) throws IllegalArgumentException { + if (name == null) throw new IllegalArgumentException("name param is null"); + if (value == null) throw new IllegalArgumentException("value param is null"); + HashMap vals = new HashMap(); + vals.put(name, value); + return resolveTemplates(vals); + } + + public KeycloakUriBuilder resolveTemplates(Map templateValues) throws IllegalArgumentException { + if (templateValues == null) throw new IllegalArgumentException("templateValues param null"); + String str = buildString(templateValues, false, true, true); + return fromTemplate(str); + } + + public KeycloakUriBuilder resolveTemplate(String name, Object value, boolean encodeSlashInPath) throws IllegalArgumentException { + if (name == null) throw new IllegalArgumentException("name param is null"); + if (value == null) throw new IllegalArgumentException("value param is null"); + HashMap vals = new HashMap(); + vals.put(name, value); + String str = buildString(vals, false, true, encodeSlashInPath); + return fromTemplate(str); + } + + public KeycloakUriBuilder resolveTemplates(Map templateValues, boolean encodeSlashInPath) throws IllegalArgumentException { + if (templateValues == null) throw new IllegalArgumentException("templateValues param null"); + String str = buildString(templateValues, false, true, encodeSlashInPath); + return fromTemplate(str); + } + + public KeycloakUriBuilder resolveTemplatesFromEncoded(Map templateValues) throws IllegalArgumentException { + if (templateValues == null) throw new IllegalArgumentException("templateValues param null"); + String str = buildString(templateValues, true, true, true); + return fromTemplate(str); + } +} diff --git a/core/src/main/java/org/keycloak/util/PathHelper.java b/core/src/main/java/org/keycloak/util/PathHelper.java new file mode 100755 index 0000000000..644f03e811 --- /dev/null +++ b/core/src/main/java/org/keycloak/util/PathHelper.java @@ -0,0 +1,62 @@ +/** + * + */ +package org.keycloak.util; + +import java.util.regex.Pattern; + + +/** + * A utility class for handling URI template parameters. As the Java + * regulare expressions package does not handle named groups, this + * class attempts to simulate that functionality by using groups. + * + * @author Ryan J. McDonough + * @author Bill Burke + * @since 1.0 + * Nov 8, 2006 + */ +public class PathHelper +{ + public static final String URI_PARAM_NAME_REGEX = "\\w[\\w\\.-]*"; + public static final String URI_PARAM_REGEX_REGEX = "[^{}][^{}]*"; + public static final String URI_PARAM_REGEX = "\\{\\s*(" + URI_PARAM_NAME_REGEX + ")\\s*(:\\s*(" + URI_PARAM_REGEX_REGEX + "))?\\}"; + public static final Pattern URI_PARAM_PATTERN = Pattern.compile(URI_PARAM_REGEX); + + /** + * A regex pattern that searches for a URI template parameter in the form of {*} + */ + public static final Pattern URI_TEMPLATE_PATTERN = Pattern.compile("(\\{([^}]+)\\})"); + + public static final char openCurlyReplacement = 6; + public static final char closeCurlyReplacement = 7; + + public static String replaceEnclosedCurlyBraces(String str) + { + char[] chars = str.toCharArray(); + int open = 0; + for (int i = 0; i < chars.length; i++) + { + if (chars[i] == '{') + { + if (open != 0) chars[i] = openCurlyReplacement; + open++; + } + else if (chars[i] == '}') + { + open--; + if (open != 0) + { + chars[i] = closeCurlyReplacement; + } + } + } + return new String(chars); + } + + public static String recoverEnclosedCurlyBraces(String str) + { + return str.replace(openCurlyReplacement, '{').replace(closeCurlyReplacement, '}'); + } + +} \ No newline at end of file diff --git a/examples/as7-eap-demo/customer-app/pom.xml b/examples/as7-eap-demo/customer-app/pom.xml index 5d88ff3b73..3fe1702680 100755 --- a/examples/as7-eap-demo/customer-app/pom.xml +++ b/examples/as7-eap-demo/customer-app/pom.xml @@ -28,26 +28,6 @@ jboss-servlet-api_3.0_spec provided - - org.jboss.resteasy - resteasy-client - provided - - - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-core-jaxrs - ${project.version} - - - org.keycloak - keycloak-adapter-core - ${project.version} - org.keycloak keycloak-as7-adapter diff --git a/examples/as7-eap-demo/customer-app/src/main/java/org/jboss/resteasy/example/oauth/CustomerDatabaseClient.java b/examples/as7-eap-demo/customer-app/src/main/java/org/jboss/resteasy/example/oauth/CustomerDatabaseClient.java deleted file mode 100755 index 2da2c84af1..0000000000 --- a/examples/as7-eap-demo/customer-app/src/main/java/org/jboss/resteasy/example/oauth/CustomerDatabaseClient.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jboss.resteasy.example.oauth; - -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; -import org.keycloak.SkeletonKeySession; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CustomerDatabaseClient -{ - public static List getCustomers(HttpServletRequest request) - { - SkeletonKeySession session = (SkeletonKeySession)request.getAttribute(SkeletonKeySession.class.getName()); - ResteasyClient client = new ResteasyClientBuilder() - .trustStore(session.getMetadata().getTruststore()) - .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build(); - try - { - Response response = client.target("http://localhost:8080/database/customers").request() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + session.getTokenString()).get(); - return response.readEntity(new GenericType>(){}); - } - finally - { - client.close(); - } - } -} diff --git a/examples/as7-eap-demo/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java b/examples/as7-eap-demo/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java new file mode 100755 index 0000000000..6da784323c --- /dev/null +++ b/examples/as7-eap-demo/customer-app/src/main/java/org/keycloak/example/CustomerDatabaseClient.java @@ -0,0 +1,49 @@ +package org.keycloak.example; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.keycloak.SkeletonKeySession; +import org.keycloak.adapters.HttpClientBuilder; +import org.keycloak.util.JsonSerialization; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CustomerDatabaseClient { + + static class TypedList extends ArrayList {} + + public static List getCustomers() { + SkeletonKeySession session = SkeletonKeySession.getContext(); + HttpClient client = new HttpClientBuilder() + .trustStore(session.getMetadata().getTruststore()) + .hostnameVerification(HttpClientBuilder.HostnameVerificationPolicy.ANY).build(); + try { + HttpGet get = new HttpGet("http://localhost:8080/database/customers"); + get.addHeader("Authorization", "Bearer " + session.getTokenString()); + try { + HttpResponse response = client.execute(get); + HttpEntity entity = response.getEntity(); + InputStream is = entity.getContent(); + try { + return JsonSerialization.readValue(is, TypedList.class); + } finally { + is.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } finally { + client.getConnectionManager().shutdown(); + } + } +} diff --git a/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 697d61c649..c54e4abec6 100755 --- a/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -2,9 +2,6 @@ - - - \ No newline at end of file diff --git a/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-web.xml b/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-web.xml index 3cec19cc47..a28a2651c9 100755 --- a/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-web.xml +++ b/examples/as7-eap-demo/customer-app/src/main/webapp/WEB-INF/jboss-web.xml @@ -1,5 +1,5 @@ - org.keycloak.adapters.as7.OAuthManagedResourceValve + org.keycloak.adapters.as7.OAuthAuthenticatorValve \ No newline at end of file diff --git a/examples/as7-eap-demo/customer-app/src/main/webapp/admin/admin.jsp b/examples/as7-eap-demo/customer-app/src/main/webapp/admin/admin.jsp old mode 100644 new mode 100755 index e132e3701f..39c2a439fe --- a/examples/as7-eap-demo/customer-app/src/main/webapp/admin/admin.jsp +++ b/examples/as7-eap-demo/customer-app/src/main/webapp/admin/admin.jsp @@ -2,7 +2,7 @@ pageEncoding="ISO-8859-1"%> - Customer Admin Iterface + Customer Admin Interface

Customer Admin Interface

diff --git a/examples/as7-eap-demo/customer-app/src/main/webapp/customers/view.jsp b/examples/as7-eap-demo/customer-app/src/main/webapp/customers/view.jsp index 7eb5f5830d..344bd3e6f6 100755 --- a/examples/as7-eap-demo/customer-app/src/main/webapp/customers/view.jsp +++ b/examples/as7-eap-demo/customer-app/src/main/webapp/customers/view.jsp @@ -1,20 +1,22 @@ -<%@ page import="javax.ws.rs.core.UriBuilder" language="java" contentType="text/html; charset=ISO-8859-1" +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> +<%@ page import="org.keycloak.example.CustomerDatabaseClient" %> +<%@ page import="org.keycloak.util.KeycloakUriBuilder" %> Customer View Page <% - String logoutUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout") - .queryParam("redirect_uri", "http://localhost:8080/customer-portal").build().toString(); - String acctUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/account").build().toString(); + String logoutUri = KeycloakUriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout") + .queryParam("redirect_uri", "http://localhost:8080/customer-portal").build().toString(); + String acctUri = "http://localhost:8080/auth-server/rest/realms/demo/account"; %>

Goto: products | logout | manage acct

User <%=request.getUserPrincipal().getName()%> made this request.

Customer Listing

<% -java.util.List list = org.jboss.resteasy.example.oauth.CustomerDatabaseClient.getCustomers(request); +java.util.List list = CustomerDatabaseClient.getCustomers(); for (String cust : list) { out.print("

"); diff --git a/examples/as7-eap-demo/database-service/pom.xml b/examples/as7-eap-demo/database-service/pom.xml index 57ef592519..fe9ea5ad64 100755 --- a/examples/as7-eap-demo/database-service/pom.xml +++ b/examples/as7-eap-demo/database-service/pom.xml @@ -30,24 +30,23 @@ provided - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-core-jaxrs - ${project.version} - - - org.keycloak - keycloak-adapter-core - ${project.version} + org.jboss.resteasy + resteasy-jaxrs + provided + org.keycloak keycloak-as7-adapter ${project.version} + diff --git a/examples/as7-eap-demo/database-service/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/as7-eap-demo/database-service/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 45b708cee3..c54e4abec6 100755 --- a/examples/as7-eap-demo/database-service/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/examples/as7-eap-demo/database-service/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -2,7 +2,6 @@ - \ No newline at end of file diff --git a/examples/as7-eap-demo/product-app/pom.xml b/examples/as7-eap-demo/product-app/pom.xml index 5054870c54..6e8bf03bc8 100755 --- a/examples/as7-eap-demo/product-app/pom.xml +++ b/examples/as7-eap-demo/product-app/pom.xml @@ -28,26 +28,6 @@ jboss-servlet-api_3.0_spec provided - - org.jboss.resteasy - resteasy-client - provided - - - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-core-jaxrs - ${project.version} - - - org.keycloak - keycloak-adapter-core - ${project.version} - org.keycloak keycloak-as7-adapter diff --git a/examples/as7-eap-demo/product-app/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java b/examples/as7-eap-demo/product-app/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java deleted file mode 100755 index dc98ec7017..0000000000 --- a/examples/as7-eap-demo/product-app/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.jboss.resteasy.example.oauth; - -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; -import org.keycloak.SkeletonKeySession; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ProductDatabaseClient -{ - public static List getProducts(HttpServletRequest request) - { - SkeletonKeySession session = (SkeletonKeySession)request.getAttribute(SkeletonKeySession.class.getName()); - ResteasyClient client = new ResteasyClientBuilder() - .trustStore(session.getMetadata().getTruststore()) - .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build(); - try - { - Response response = client.target("http://localhost:8080/database/products").request() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + session.getTokenString()).get(); - return response.readEntity(new GenericType>(){}); - } - finally - { - client.close(); - } - } -} diff --git a/examples/as7-eap-demo/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java b/examples/as7-eap-demo/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java new file mode 100755 index 0000000000..61dcec6c13 --- /dev/null +++ b/examples/as7-eap-demo/product-app/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java @@ -0,0 +1,49 @@ +package org.keycloak.example.oauth; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.keycloak.SkeletonKeySession; +import org.keycloak.adapters.HttpClientBuilder; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ProductDatabaseClient +{ + static class TypedList extends ArrayList {} + + public static List getProducts() { + SkeletonKeySession session = SkeletonKeySession.getContext(); + HttpClient client = new HttpClientBuilder() + .trustStore(session.getMetadata().getTruststore()) + .hostnameVerification(HttpClientBuilder.HostnameVerificationPolicy.ANY).build(); + try { + HttpGet get = new HttpGet("http://localhost:8080/database/products"); + get.addHeader("Authorization", "Bearer " + session.getTokenString()); + try { + HttpResponse response = client.execute(get); + HttpEntity entity = response.getEntity(); + InputStream is = entity.getContent(); + try { + return JsonSerialization.readValue(is, TypedList.class); + } finally { + is.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } finally { + client.getConnectionManager().shutdown(); + } + } + +} diff --git a/examples/as7-eap-demo/product-app/src/main/webapp/WEB-INF/jboss-web.xml b/examples/as7-eap-demo/product-app/src/main/webapp/WEB-INF/jboss-web.xml index 3cec19cc47..a28a2651c9 100755 --- a/examples/as7-eap-demo/product-app/src/main/webapp/WEB-INF/jboss-web.xml +++ b/examples/as7-eap-demo/product-app/src/main/webapp/WEB-INF/jboss-web.xml @@ -1,5 +1,5 @@ - org.keycloak.adapters.as7.OAuthManagedResourceValve + org.keycloak.adapters.as7.OAuthAuthenticatorValve \ No newline at end of file diff --git a/examples/as7-eap-demo/product-app/src/main/webapp/products/view.jsp b/examples/as7-eap-demo/product-app/src/main/webapp/products/view.jsp index 88c6493906..cd3d8d0ac9 100755 --- a/examples/as7-eap-demo/product-app/src/main/webapp/products/view.jsp +++ b/examples/as7-eap-demo/product-app/src/main/webapp/products/view.jsp @@ -1,21 +1,23 @@ -<%@ page import="javax.ws.rs.core.UriBuilder" language="java" contentType="text/html; charset=ISO-8859-1" +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> +<%@ page import="org.keycloak.example.oauth.ProductDatabaseClient" %> +<%@ page import="org.keycloak.util.KeycloakUriBuilder" %> Product View Page <% - String logoutUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout") + String logoutUri = KeycloakUriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout") .queryParam("redirect_uri", "http://localhost:8080/product-portal").build().toString(); - String acctUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/account").build().toString(); + String acctUri = "http://localhost:8080/auth-server/rest/realms/demo/account"; %>

Goto: customers | logout | manage acct

User <%=request.getUserPrincipal().getName()%> made this request.

Product Listing

<% -java.util.List list = org.jboss.resteasy.example.oauth.ProductDatabaseClient.getProducts(request); +java.util.List list = ProductDatabaseClient.getProducts(); for (String cust : list) { out.print("

"); diff --git a/examples/as7-eap-demo/third-party/pom.xml b/examples/as7-eap-demo/third-party/pom.xml index 04308a90e0..89372bcdf7 100755 --- a/examples/as7-eap-demo/third-party/pom.xml +++ b/examples/as7-eap-demo/third-party/pom.xml @@ -21,19 +21,9 @@ 1.0.1.Final provided - - org.jboss.resteasy - resteasy-client - provided - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-core-jaxrs + keycloak-servlet-oauth-client ${project.version} diff --git a/examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/Bootstrap.java b/examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java similarity index 93% rename from examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/Bootstrap.java rename to examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java index 717cd3e5a1..121af39b0a 100755 --- a/examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/Bootstrap.java +++ b/examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/Bootstrap.java @@ -1,6 +1,5 @@ -package org.jboss.resteasy.example.oauth; +package org.keycloak.example.oauth; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.keycloak.servlet.ServletOAuthClient; import javax.servlet.ServletContextEvent; @@ -55,7 +54,6 @@ public class Bootstrap implements ServletContextListener { client.setPassword("password"); client.setAuthUrl("http://localhost:8080/auth-server/rest/realms/demo/tokens/login"); client.setCodeUrl("http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes"); - client.setClient(new ResteasyClientBuilder().build()); client.start(); sce.getServletContext().setAttribute(ServletOAuthClient.class.getName(), client); diff --git a/examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java b/examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java similarity index 54% rename from examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java rename to examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java index d21c823bd8..2b4cbd5c72 100755 --- a/examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java +++ b/examples/as7-eap-demo/third-party/src/main/java/org/keycloak/example/oauth/ProductDatabaseClient.java @@ -1,15 +1,18 @@ -package org.jboss.resteasy.example.oauth; +package org.keycloak.example.oauth; -import org.jboss.resteasy.client.jaxrs.ResteasyClient; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.keycloak.adapters.TokenGrantRequest; import org.keycloak.servlet.ServletOAuthClient; +import org.keycloak.util.JsonSerialization; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.List; /** @@ -31,6 +34,8 @@ public class ProductDatabaseClient { } } + static class TypedList extends ArrayList {} + public static List getProducts(HttpServletRequest request) { // This is really the worst code ever. The ServletOAuthClient is obtained by getting a context attribute // that is set in the Bootstrap context listenr in this project. @@ -38,32 +43,30 @@ public class ProductDatabaseClient { // and obtain the ServletOAuthClient. I actually suggest downloading the ServletOAuthClient code // and take a look how it works. ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(ServletOAuthClient.class.getName()); - String token = oAuthClient.getBearerToken(request); - ResteasyClient client = new ResteasyClientBuilder() - .trustStore(oAuthClient.getTruststore()) - .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build(); + String token = null; try { - // invoke without the Authorization header - Response response = client.target("http://localhost:8080/database/products").request().get(); - response.close(); - if (response.getStatus() != 401) { - response.close(); - client.close(); - throw new RuntimeException("Expecting an auth status code: " + response.getStatus()); - } - } finally { + token = oAuthClient.getBearerToken(request); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (TokenGrantRequest.HttpFailure failure) { + throw new RuntimeException(failure); } + + HttpClient client = oAuthClient.getClient(); + + HttpGet get = new HttpGet("http://localhost:8080/database/products"); + get.addHeader("Authorization", "Bearer " + token); try { - Response response = client.target("http://localhost:8080/database/products").request() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get(); - if (response.getStatus() != 200) { - response.close(); - throw new RuntimeException("Failed to access!: " + response.getStatus()); + HttpResponse response = client.execute(get); + HttpEntity entity = response.getEntity(); + InputStream is = entity.getContent(); + try { + return JsonSerialization.readValue(is, TypedList.class); + } finally { + is.close(); } - return response.readEntity(new GenericType>() { - }); - } finally { - client.close(); + } catch (IOException e) { + throw new RuntimeException(e); } } } diff --git a/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 74f5dff8db..c54e4abec6 100755 --- a/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -2,8 +2,6 @@ - - \ No newline at end of file diff --git a/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml b/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml index 501b203606..958839db9f 100755 --- a/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml +++ b/examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml @@ -7,7 +7,7 @@ oauth-client - org.jboss.resteasy.example.oauth.Bootstrap + org.keycloak.example.oauth.Bootstrap diff --git a/integration/servlet-oauth-client/pom.xml b/integration/servlet-oauth-client/pom.xml new file mode 100755 index 0000000000..038f1aba83 --- /dev/null +++ b/integration/servlet-oauth-client/pom.xml @@ -0,0 +1,67 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-servlet-oauth-client + Keycloak Servlet OAuth Client + + + + + org.bouncycastle + bcprov-jdk16 + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-adapter-core + ${project.version} + + + org.apache.httpcomponents + httpclient + 4.1.2 + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/core-jaxrs/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java similarity index 61% rename from core-jaxrs/src/main/java/org/keycloak/servlet/ServletOAuthClient.java rename to integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java index 95ea522131..8799bab92c 100755 --- a/core-jaxrs/src/main/java/org/keycloak/servlet/ServletOAuthClient.java +++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -1,23 +1,58 @@ package org.keycloak.servlet; -import org.jboss.resteasy.plugins.server.servlet.ServletUtil; -import org.jboss.resteasy.spi.ResteasyUriInfo; +import org.apache.http.client.HttpClient; import org.keycloak.AbstractOAuthClient; +import org.keycloak.adapters.HttpClientBuilder; +import org.keycloak.adapters.TokenGrantRequest; +import org.keycloak.util.KeycloakUriBuilder; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.core.UriBuilder; import java.io.IOException; import java.net.URI; +import java.util.HashMap; +import java.util.Map; /** * @author Bill Burke * @version $Revision: 1 $ */ public class ServletOAuthClient extends AbstractOAuthClient { + protected HttpClient client; + + /** + * Creates a Client for obtaining access token from code + */ + public void start() { + if (client == null) { + client = new HttpClientBuilder().trustStore(truststore) + .hostnameVerification(HttpClientBuilder.HostnameVerificationPolicy.ANY) + .connectionPoolSize(10) + .build(); + } + } + + /** + * closes cllient + */ + public void stop() { + client.getConnectionManager().shutdown(); + } + public HttpClient getClient() { + return client; + } + + public void setClient(HttpClient client) { + this.client = client; + } + + public String resolveBearerToken(String redirectUri, String code) throws IOException, TokenGrantRequest.HttpFailure { + Map credentials = new HashMap(); + credentials.put("password", password); + return TokenGrantRequest.invoke(client, code, codeUrl, redirectUri, clientId, credentials).getToken(); + } + /** * Start the process of obtaining an access token by redirecting the browser * to the authentication server @@ -28,8 +63,11 @@ public class ServletOAuthClient extends AbstractOAuthClient { * @throws IOException */ public void redirectRelative(String relativePath, HttpServletRequest request, HttpServletResponse response) throws IOException { - ResteasyUriInfo uriInfo = ServletUtil.extractUriInfo(request, null); - String redirect = uriInfo.getBaseUriBuilder().path(relativePath).toTemplate(); + KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(request.getRequestURL().toString()) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .path(relativePath); + String redirect = builder.toTemplate(); redirect(redirect, request, response); } @@ -46,7 +84,7 @@ public class ServletOAuthClient extends AbstractOAuthClient { public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException { String state = getStateCode(); - URI url = UriBuilder.fromUri(authUrl) + URI url = KeycloakUriBuilder.fromUri(authUrl) .queryParam("client_id", clientId) .queryParam("redirect_uri", redirectUri) .queryParam("state", state) @@ -92,24 +130,24 @@ public class ServletOAuthClient extends AbstractOAuthClient { * * @param request * @return - * @throws BadRequestException - * @throws InternalServerErrorException + * @throws IOException + * @throws org.keycloak.adapters.TokenGrantRequest.HttpFailure */ - public String getBearerToken(HttpServletRequest request) throws BadRequestException, InternalServerErrorException { + public String getBearerToken(HttpServletRequest request) throws IOException, TokenGrantRequest.HttpFailure { String error = request.getParameter("error"); - if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error)); + if (error != null) throw new IOException("OAuth error: " + error); String redirectUri = request.getRequestURL().append("?").append(request.getQueryString()).toString(); String stateCookie = getCookieValue(stateCookieName, request); - if (stateCookie == null) throw new BadRequestException(new Exception("state cookie not set")); + if (stateCookie == null) throw new IOException("state cookie not set"); // we can call get parameter as this should be a redirect String state = request.getParameter("state"); String code = request.getParameter("code"); - if (state == null) throw new BadRequestException(new Exception("state parameter was null")); + if (state == null) throw new IOException("state parameter was null"); if (!state.equals(stateCookie)) { - throw new BadRequestException(new Exception("state parameter invalid")); + throw new IOException("state parameter invalid"); } - if (code == null) throw new BadRequestException(new Exception("code parameter was null")); + if (code == null) throw new IOException("code parameter was null"); return resolveBearerToken(redirectUri, code); } diff --git a/integration/undertow/pom.xml b/integration/undertow/pom.xml index 3060d396ed..f3ae801d00 100755 --- a/integration/undertow/pom.xml +++ b/integration/undertow/pom.xml @@ -23,29 +23,38 @@ org.keycloak keycloak-core ${project.version} - provided org.keycloak keycloak-adapter-core ${project.version} - provided + + + org.apache.httpcomponents + httpclient + 4.1.2 + + + org.bouncycastle + bcprov-jdk16 + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-xc org.jboss.spec.javax.servlet jboss-servlet-api_3.0_spec provided - - org.jboss.resteasy - resteasy-jaxrs - provided - - - org.jboss.resteasy - resteasy-client - provided - io.undertow diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java index ef7120eed9..749c6b0d11 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakAuthenticationMechanism.java @@ -6,7 +6,7 @@ import io.undertow.security.idm.Account; import io.undertow.server.HttpServerExchange; import io.undertow.util.AttachmentKey; import org.jboss.logging.Logger; -import org.keycloak.adapters.RealmConfiguration; +import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.ResourceMetadata; import org.keycloak.SkeletonKeyPrincipal; import org.keycloak.SkeletonKeySession; diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java index 16f71bdddb..02a39d4094 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java @@ -8,6 +8,7 @@ import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.ServletSessionConfig; import org.jboss.logging.Logger; import org.keycloak.adapters.config.AdapterConfig; +import org.keycloak.adapters.config.RealmConfigurationLoader; import javax.servlet.ServletContext; import java.io.InputStream; diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java index 8806ac6eb4..433feb263b 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/OAuthAuthenticator.java @@ -8,16 +8,14 @@ import io.undertow.server.handlers.CookieImpl; import io.undertow.util.Headers; import org.jboss.logging.Logger; import org.keycloak.RSATokenVerifier; -import org.keycloak.adapters.RealmConfiguration; +import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.VerificationException; +import org.keycloak.adapters.TokenGrantRequest; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.SkeletonKeyToken; -import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.util.KeycloakUriBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; +import java.io.IOException; import java.util.Deque; import java.util.Map; import java.util.UUID; @@ -60,7 +58,7 @@ public class OAuthAuthenticator { } protected String getRequestUrl() { - UriBuilder uriBuilder = UriBuilder.fromUri(exchange.getRequestURI()) + KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(exchange.getRequestURI()) .replaceQuery(exchange.getQueryString()); if (!exchange.isHostIncludedInRequestURI()) uriBuilder.scheme(exchange.getRequestScheme()).host(exchange.getHostAndPort()); return uriBuilder.build().toString(); @@ -107,7 +105,7 @@ public class OAuthAuthenticator { // disabled? return null; } - UriBuilder secureUrl = UriBuilder.fromUri(url).scheme("https").port(-1); + KeycloakUriBuilder secureUrl = KeycloakUriBuilder.fromUri(url).scheme("https").port(-1); if (port != 443) secureUrl.port(port); url = secureUrl.build().toString(); } @@ -237,34 +235,20 @@ public class OAuthAuthenticator { KeycloakChallenge challenge = checkStateCookie(); if (challenge != null) return challenge; - String client_id = realmInfo.getMetadata().getResourceName(); - String password = realmInfo.getResourceCredentials().asMap().getFirst("password"); - //String authHeader = BasicAuthHelper.createHeader(client_id, password); - redirectUri = stripOauthParametersFromRedirect(); - Form form = new Form(); - form.param("grant_type", "authorization_code") - .param("code", code) - .param("client_id", client_id) - .param(CredentialRepresentation.PASSWORD, password) - .param("redirect_uri", redirectUri); - - Response res = realmInfo.getCodeUrl().request() - .post(Entity.form(form)); - AccessTokenResponse tokenResponse; + AccessTokenResponse tokenResponse = null; try { - if (res.getStatus() != 200) { - log.error("failed to turn code into token"); - log.error("status from server: " + res.getStatus()); - if (res.getStatus() == 400 && res.getMediaType() != null) { - log.error(" " + res.readEntity(String.class)); - } - return challenge(403); + tokenResponse = TokenGrantRequest.invoke(realmInfo, code, redirectUri); + } catch (TokenGrantRequest.HttpFailure failure) { + log.error("failed to turn code into token"); + log.error("status from server: " + failure.getStatus()); + if (failure.getStatus() == 400 && failure.getError() != null) { + log.error(" " + failure.getError()); } - log.debug("media type: " + res.getMediaType()); - log.debug("Content-Type header: " + res.getHeaderString("Content-Type")); - tokenResponse = res.readEntity(AccessTokenResponse.class); - } finally { - res.close(); + return challenge(403); + + } catch (IOException e) { + log.error("failed to turn code into token"); + return challenge(403); } tokenString = tokenResponse.getToken(); @@ -283,7 +267,7 @@ public class OAuthAuthenticator { * strip out unwanted query parameters and redirect so bookmarks don't retain oauth protocol bits */ protected String stripOauthParametersFromRedirect() { - UriBuilder builder = UriBuilder.fromUri(exchange.getRequestURI()) + KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(exchange.getRequestURI()) .replaceQuery(exchange.getQueryString()) .replaceQueryParam("code", null) .replaceQueryParam("state", null); diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthenticationMechanism.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthenticationMechanism.java index 5c337177b9..9df6863d78 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthenticationMechanism.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthenticationMechanism.java @@ -3,7 +3,7 @@ package org.keycloak.adapters.undertow; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.api.ConfidentialPortManager; import io.undertow.servlet.handlers.ServletRequestContext; -import org.keycloak.adapters.RealmConfiguration; +import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.ResourceMetadata; import org.keycloak.SkeletonKeySession; import org.keycloak.adapters.config.AdapterConfig; diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletOAuthAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletOAuthAuthenticator.java index ce807a6e21..e891a1d024 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletOAuthAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletOAuthAuthenticator.java @@ -2,7 +2,7 @@ package org.keycloak.adapters.undertow; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.api.ConfidentialPortManager; -import org.keycloak.adapters.RealmConfiguration; +import org.keycloak.adapters.config.RealmConfiguration; /** * @author Bill Burke diff --git a/pom.xml b/pom.xml index fc62fe809e..0921449b83 100755 --- a/pom.xml +++ b/pom.xml @@ -76,8 +76,8 @@ core core-jaxrs model - services integration + services social forms admin-ui-styles @@ -327,7 +327,14 @@ mysql-connector-java ${mysql.version} - + + diff --git a/services/pom.xml b/services/pom.xml index 56e84810a7..3c1e0fbf58 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -35,6 +35,12 @@ keycloak-model-api ${project.version} + + org.keycloak + keycloak-jaxrs-oauth-client + ${project.version} + + org.keycloak keycloak-model-jpa @@ -83,7 +89,7 @@ - + org.jboss.resteasy jaxrs-api provided @@ -162,11 +168,11 @@ hibernate-entitymanager test - - com.icegreen - greenmail + + com.icegreen + greenmail test - + diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java index f6cc0d0558..f6a49112e4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java @@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; +import java.net.URI; import java.nio.charset.Charset; import java.util.List; @@ -34,7 +35,13 @@ public class DummySocialServlet extends HttpServlet { String state = null; String redirectUri = null; - List query = URLEncodedUtils.parse(req.getQueryString(), Charset.forName("UTF-8")); + List query = null; + try { + URI uri = URI.create(req.getRequestURL().append('?').append(req.getQueryString()).toString()); + query = URLEncodedUtils.parse(uri, "UTF-8"); + } catch (Exception e) { + throw new RuntimeException(e); + } for (NameValuePair p : query) { if ("state".equals(p.getName())) { state = p.getValue(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 6b6e1d5aa6..49acb71b19 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -43,6 +43,7 @@ import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import javax.ws.rs.core.UriBuilder; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; @@ -127,7 +128,12 @@ public class OAuthClient { parameters.add(new BasicNameValuePair("password", password)); } - UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(parameters, Charset.forName("UTF-8")); + UrlEncodedFormEntity formEntity = null; + try { + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } post.setEntity(formEntity); try {