KEYCLOAK-3564 Update demo examples with public key rotation

This commit is contained in:
mposolda 2016-10-04 14:04:40 +02:00
parent f8c236afa8
commit bc916a1909
15 changed files with 78 additions and 209 deletions

View file

@ -28,7 +28,7 @@ _This demo is meant to run on the same server instance as the Keycloak Server!_
Step 1: Make sure you've set up the Keycloak Server
--------------------------------------
The Keycloak Appliance Distribution comes with a preconfigured Keycloak server (based on Wildfly). You can use it out of
The Keycloak Demo Distribution comes with a preconfigured Keycloak server (based on Wildfly). You can use it out of
the box to run these demos. So, if you're using this, you can head to Step 2.
Alternatively, you can install the Keycloak Server onto any EAP 6.x, or Wildfly 8.x server, but there is
@ -157,8 +157,8 @@ are still happening, but the auth-server knows you are already logged in so the
If you click on the logout link of either of the product or customer app, you'll be logged out of all the applications.
If you click on [http://localhost:8080/customer-portal-js](http://localhost:8080/customer-portal-js) you can invoke
on the pure HTML/Javascript application.
The example also shows different methods of client authentication. The customer-portal example is using traditional authentication with client_id and client_secret,
but the product-portal example is using client authentication with JWT signed by client private key, which is retrieved from the keystore file inside the product-portal WAR.
Step 6: Traditional OAuth2 Example
----------------------------------
@ -240,9 +240,6 @@ An example for retrieve service account dedicated to the Client Application itse
Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
The example also shows different methods of client authentication. There is ProductSAClientSecretServlet using traditional authentication with clientId and client_secret,
but there is also ProductSAClientSignedJWTServlet using client authentication with JWT signed by client private key.
Step 13: Offline Access Example
===============================
An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token

View file

@ -44,9 +44,8 @@ You also need to add realm config to the same file. Add a new child-element to `
<profile>
....
<subsystem xmlns="urn:jboss:domain:keycloak:1.0">
<subsystem xmlns="urn:jboss:domain:keycloak:1.1">
<realm name="demo">
<realm-public-key>REALM PUBLIC KEY</realm-public-key>
<auth-server-url>KEYCLOAK URL</auth-server-url>
<ssl-required>external</ssl-required>
</realm>
@ -55,7 +54,6 @@ You also need to add realm config to the same file. Add a new child-element to `
In the above snippet replace the following:
* `REALM PUBLIC KEY` - replace with the public key for the realm. You can find this in the admin console by selecting the realm, then clicking on `Keys`
* `KEYCLOAK URL` - replace with the base url of Keycloak (for example http://localhost:8080/auth or http://keycloak.example.org/auth)
Don't start the WildFly server until you've configured and deployed the demo applications.
@ -70,7 +68,7 @@ Run the following to deploy it:
# mvn install
# cp target/database.war <WILDFLY HOME>/standalone/deployments
Next add the configuration for it to the Keycloak subsystem. Edit `<WILDFLY HOME>/standalone/configuration/standalone.xml` to `<subsystem xmlns="urn:jboss:domain:keycloak:1.0">` add:
Next add the configuration for it to the Keycloak subsystem. Edit `<WILDFLY HOME>/standalone/configuration/standalone.xml` to `<subsystem xmlns="urn:jboss:domain:keycloak:1.1">` add:
<secure-deployment name="database.war">
<realm>demo</realm>
@ -88,12 +86,17 @@ Run the following to deploy it:
# mvn install
# cp target/customer-portal.war <WILDFLY HOME>/standalone/deployments
Then open the Keycloak admin console to add a configuration for it. Navigate to the realm and click on `Applications` then `Add Application`. Fill in the form with:
Then open the Keycloak admin console to add a configuration for it. Navigate to the realm and click on `Clients` then `Add Client`. Fill in the form with:
* Name - `customer-portal`
* Redirect URI - `http://localhost:8080/customer-portal/*` (click `Add` after filling in the field)
* Client ID - `customer-portal`
Then click on `Save`. As it's a confidential (non-public) application you need the secret for it. Click on `Credentials` and note the value of the `Secret` field.
Then click on `Save`. You will see more possibilities to setup client now, so you can add the following:
`Access Type` - `confidential`
`Valid Redirect URIs` - `http://localhost:8080/customer-portal/*` (click `Add` after filling in the field)
Then click on `Save` again so that client is updated.
As it's a confidential (non-public) application you need the secret for it. Click on `Credentials` and note the value of the `Secret` field.
Then edit `<WILDFLY HOME>/standalone/configuration/standalone.xml` and add the following to `<subsystem xmlns="urn:jboss:domain:keycloak:1.0">`:
@ -117,21 +120,39 @@ Run the following to deploy it:
# mvn install
# cp target/product-portal.war <WILDFLY HOME>/standalone/deployments
Then open the Keycloak admin console to add a configuration for it. Navigate to the realm and click on `Applications` then `Add Application`. Fill in the form with:
Then open the Keycloak admin console to add a configuration for it. Navigate to the realm and click on `Clients` then `Add Client`. Fill in the form with:
* Name - `product-portal`
* Redirect URI - `http://localhost:8080/product-portal/*` (click `Add` after filling in the field)
* Client ID - `product-portal`
Then click on `Save`. As it's a confidential (non-public) application you need the secret for it. Click on `Credentials` and note the value of the `Secret` field.
Then click on `Save`. You will see more possibilities to setup client now, so you can add the following:
`Access Type` - `confidential`
`Valid Redirect URIs` - `http://localhost:8080/product-portal/*` (click `Add` after filling in the field)
Then click on `Save` again so that client is updated.
It's a confidential (non-public) application, so we again need client credentials for it. But for product-portal, we will use authentication with signed JWT instead of traditional OAuth2 client secret.
Click on `Credentials` and fill the following values:
`Client Authenticator` - `Signed JWT`
`Use JWKS URL` - `ON`
`JWKS URL` - `/product-portal/k_jwks`
Then edit `<WILDFLY HOME>/standalone/configuration/standalone.xml` and add the following to `<subsystem xmlns="urn:jboss:domain:keycloak:1.0">`:
<secure-deployment name="product-portal.war">
<realm>demo</realm>
<resource>product-portal</resource>
<credential name="secret">APPLICATION SECRET</credential>
<credential name="jwt">
<client-keystore-file>classpath:keystore-client.jks</client-keystore-file>
<client-keystore-type>JKS</client-keystore-type>
<client-keystore-password>storepass</client-keystore-password>
<client-key-password>keypass</client-key-password>
<client-key-alias>clientkey</client-key-alias>
<token-expiration>10</token-expiration>
</credential>
</secure-deployment>
In the above snippet replace the following:
* `APPLICATION SECRET` - replace with the applications secret you just noted from the Keycloak admin console
With this configuration, the product-portal application will authenticate with JWT token signed by the private key from the file `keystore-client.jks`, which is available
inside the application WAR. If you don't use `classpath:` prefix in the configuration, you can use any keystore file from filesystem. If you want to generate your own keystore file,
you can either use `keytool` tool, but you can also generate the one inside Keycloak admin console and then save it locally.

View file

@ -3,7 +3,14 @@
"resource" : "product-portal",
"auth-server-url" : "/auth",
"ssl-required" : "external",
"credentials" : {
"secret": "password"
"credentials": {
"jwt": {
"client-keystore-file": "classpath:keystore-client.jks",
"client-keystore-type": "JKS",
"client-keystore-password": "storepass",
"client-key-password": "keypass",
"client-key-alias": "clientkey",
"token-expiration": 10
}
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.example;
/**
* Client authentication with traditional OAuth2 client_id + client_secret
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductSAClientSecretServlet extends ProductServiceAccountServlet {
@Override
protected String getAdapterConfigLocation() {
return "/WEB-INF/keycloak-client-secret.json";
}
@Override
protected String getClientAuthenticationMethod() {
return "secret";
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.example;
/**
* Client authentication based on JWT signed by client private key .
* See Keycloak documentation and <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
@Override
protected String getAdapterConfigLocation() {
return "/WEB-INF/keycloak-client-signed-jwt.json";
}
@Override
protected String getClientAuthenticationMethod() {
return "jwt";
}
}

View file

@ -33,6 +33,7 @@ import org.keycloak.adapters.ServerRequest;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.adapters.rotation.AdapterRSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.common.util.UriUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@ -53,36 +54,28 @@ import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class ProductServiceAccountServlet extends HttpServlet {
public class ProductServiceAccountServlet extends HttpServlet {
public static final String ERROR = "error";
public static final String TOKEN = "token";
public static final String TOKEN_PARSED = "idTokenParsed";
public static final String REFRESH_TOKEN = "refreshToken";
public static final String PRODUCTS = "products";
public static final String CLIENT_AUTH_METHOD = "clientAuthMethod";
protected abstract String getAdapterConfigLocation();
protected abstract String getClientAuthenticationMethod();
public static String getLoginUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/login";
}
public static String getRefreshUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/refresh";
return "/service-account-portal/app/login";
}
public static String getLogoutUrl(HttpServletRequest request) {
return "/service-account-portal/app-" + request.getAttribute(CLIENT_AUTH_METHOD) + "/logout";
return "/service-account-portal/app/logout";
}
@Override
public void init() throws ServletException {
String adapterConfigLocation = getAdapterConfigLocation();
String adapterConfigLocation = "/WEB-INF/keycloak.json";
InputStream config = getServletContext().getResourceAsStream(adapterConfigLocation);
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
getServletContext().setAttribute("deployment-" + getClientAuthenticationMethod(), deployment);
getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment);
HttpClient client = new DefaultHttpClient();
getServletContext().setAttribute(HttpClient.class.getName(), client);
@ -95,13 +88,10 @@ public abstract class ProductServiceAccountServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute(CLIENT_AUTH_METHOD, getClientAuthenticationMethod());
String reqUri = req.getRequestURI();
if (reqUri.endsWith("/login")) {
serviceAccountLogin(req);
} else if (reqUri.endsWith("/refresh")) {
refreshToken(req);
} else if (reqUri.endsWith("/logout")){
logout(req);
}
@ -197,25 +187,6 @@ public abstract class ProductServiceAccountServlet extends HttpServlet {
}
}
private void refreshToken(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
if (refreshToken == null) {
req.setAttribute(ERROR, "No refresh token available. Please login first");
} else {
try {
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
setTokens(req, deployment, tokenResponse);
} catch (ServerRequest.HttpFailure hfe) {
hfe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
} catch (Exception ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
}
}
}
private void logout(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
@ -240,27 +211,11 @@ public abstract class ProductServiceAccountServlet extends HttpServlet {
private String getContent(HttpEntity entity) throws IOException {
if (entity == null) return null;
InputStream is = entity.getContent();
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
while ((c = is.read()) != -1) {
os.write(c);
}
byte[] bytes = os.toByteArray();
String data = new String(bytes);
return data;
} finally {
try {
is.close();
} catch (IOException ignored) {
}
}
return StreamUtil.readString(is);
}
private KeycloakDeployment getKeycloakDeployment() {
return (KeycloakDeployment) getServletContext().getAttribute("deployment-" + getClientAuthenticationMethod());
return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName());
}
private HttpClient getHttpClient() {

View file

@ -1,16 +0,0 @@
{
"realm" : "demo",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"resource" : "product-sa-client-jwt-auth",
"credentials": {
"jwt": {
"client-keystore-file": "classpath:keystore-client.jks",
"client-keystore-type": "JKS",
"client-keystore-password": "storepass",
"client-key-password": "keypass",
"client-key-alias": "clientkey",
"token-expiration": 10
}
}
}

View file

@ -13,16 +13,12 @@
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
String clientAuthMethod = (String) request.getAttribute(ProductServiceAccountServlet.CLIENT_AUTH_METHOD);
String loginUrl = ProductServiceAccountServlet.getLoginUrl(request);
String refreshUrl = ProductServiceAccountServlet.getRefreshUrl(request);
String logoutUrl = ProductServiceAccountServlet.getLogoutUrl(request);
%>
<h1>Service account portal</h1>
<h2>Client authentication method: <%= clientAuthMethod %></h2>
<p><a href="<%= loginUrl %>">Login</a> | <a href="<%= refreshUrl %>">Refresh token</a> | <a
href="<%= logoutUrl %>">Logout</a></p>
<p><a href="<%= loginUrl %>">Login</a> | <a href="<%= logoutUrl %>">Logout</a></p>
<hr />
<% if (appError != null) { %>

View file

@ -25,22 +25,12 @@
<servlet>
<servlet-name>ProductSAClientSecretServlet</servlet-name>
<servlet-class>org.keycloak.example.ProductSAClientSecretServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
<servlet-class>org.keycloak.example.ProductSAClientSignedJWTServlet</servlet-class>
<servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ProductSAClientSecretServlet</servlet-name>
<url-pattern>/app-secret/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>ProductSAClientSignedJWTServlet</servlet-name>
<url-pattern>/app-jwt/*</url-pattern>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>

View file

@ -16,11 +16,7 @@
-->
<html>
<head><title>Service account example</title></head>
<body>
<ul>
<li><a href="app-secret">Client authentication with client secret</a><br /><br /></li>
<li><a href="app-jwt">Client authentication with JWT signed by client private key</a></li>
</ul>
</body>
<head>
<meta http-equiv="Refresh" content="0; URL=app">
</head>
</html>

View file

@ -18,7 +18,6 @@
<!-- works with keycloak.json that comes with example -->
<subsystem xmlns="urn:jboss:domain:keycloak:1.0">
<realm name="demo">
<realm-public-key>MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</realm-public-key>
<auth-server-url>/auth</auth-server-url>
<ssl-required>external</ssl-required>
</realm>
@ -30,7 +29,14 @@
<secure-deployment name="product-portal.war">
<realm>demo</realm>
<resource>product-portal</resource>
<credential name="secret">password</credential>
<credential name="jwt">
<client-keystore-file>classpath:keystore-client.jks</client-keystore-file>
<client-keystore-type>JKS</client-keystore-type>
<client-keystore-password>storepass</client-keystore-password>
<client-key-password>keypass</client-key-password>
<client-key-alias>clientkey</client-key-alias>
<token-expiration>10</token-expiration>
</credential>
</secure-deployment>
<secure-deployment name="database.war">
<realm>demo</realm>

View file

@ -79,13 +79,6 @@
"email" : "service-account-product-sa-client@placeholder.org",
"serviceAccountClientId": "product-sa-client",
"realmRoles": [ "user" ]
},
{
"username" : "service-account-product-sa-client-jwt-auth",
"enabled": true,
"email" : "service-account-product-sa-client-jwt-auth@placeholder.org",
"serviceAccountClientId": "product-sa-client-jwt-auth",
"realmRoles": [ "user" ]
}
],
"roles" : {
@ -175,7 +168,11 @@
"redirectUris": [
"/product-portal/*"
],
"secret": "password"
"clientAuthenticatorType": "client-jwt",
"attributes": {
"use.jwks.url": "true",
"jwks.url": "/product-portal/k_jwks"
}
},
{
"clientId": "database-service",
@ -207,15 +204,6 @@
"secret": "password",
"serviceAccountsEnabled": true
},
{
"clientId": "product-sa-client-jwt-auth",
"enabled": true,
"serviceAccountsEnabled": true,
"clientAuthenticatorType": "client-jwt",
"attributes": {
"jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
}
},
{
"clientId": "offline-access-portal",
"enabled": true,

View file

@ -36,6 +36,7 @@ import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.CertificateInfoHelper;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.util.JWKSUtils;
/**
@ -59,6 +60,7 @@ public class ClientPublicKeyLoader implements PublicKeyLoader {
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
if (config.isUseJwksUrl()) {
String jwksUrl = config.getJwksUrl();
jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl);
JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl);
return JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG);
} else {