From 1a34feda651d85730327cb1505c9472cb1591412 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 22 Jul 2015 10:53:41 +0200 Subject: [PATCH] KEYCLOAK-401 Added service account demo --- examples/demo-template/README.md | 8 + examples/demo-template/pom.xml | 1 + .../demo-template/service-account/pom.xml | 60 +++++ .../example/ProductServiceAccountServlet.java | 234 ++++++++++++++++++ .../WEB-INF/jboss-deployment-structure.xml | 9 + .../src/main/webapp/WEB-INF/keycloak.json | 10 + .../src/main/webapp/WEB-INF/page.jsp | 52 ++++ .../src/main/webapp/WEB-INF/web.xml | 19 ++ .../src/main/webapp/index.html | 5 + examples/demo-template/testrealm.json | 6 + 10 files changed, 404 insertions(+) create mode 100644 examples/demo-template/service-account/pom.xml create mode 100644 examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java create mode 100644 examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml create mode 100644 examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json create mode 100644 examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp create mode 100644 examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml create mode 100644 examples/demo-template/service-account/src/main/webapp/index.html diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md index 1a896af191..2445e31142 100755 --- a/examples/demo-template/README.md +++ b/examples/demo-template/README.md @@ -210,6 +210,14 @@ An pure HTML5/Javascript example using Keycloak to secure it. If you are already logged in, you will not be asked for a username and password, but you will be redirected to an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app. +Step 10: Service Account Example +================================ +An example for retrieve service account dedicated to the Client Application itself (not to any user). + +[http://localhost:8080/service-account-portal](http://localhost:8080/service-account-portal) + +Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) + Admin Console ========================== diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml index a0172c85e5..4ea8c39e81 100755 --- a/examples/demo-template/pom.xml +++ b/examples/demo-template/pom.xml @@ -36,6 +36,7 @@ database-service third-party third-party-cdi + service-account diff --git a/examples/demo-template/service-account/pom.xml b/examples/demo-template/service-account/pom.xml new file mode 100644 index 0000000000..fde69667a0 --- /dev/null +++ b/examples/demo-template/service-account/pom.xml @@ -0,0 +1,60 @@ + + + + keycloak-examples-demo-parent + org.keycloak + 1.4.0.Final-SNAPSHOT + + + 4.0.0 + org.keycloak.example.demo + service-account-example + war + Service Account Example App + + + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-adapter-core + provided + + + org.apache.httpcomponents + httpclient + provided + + + + + service-account-portal + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + + \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java new file mode 100644 index 0000000000..f9dc9f165e --- /dev/null +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -0,0 +1,234 @@ +package org.keycloak.example; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.keycloak.OAuth2Constants; +import org.keycloak.RSATokenVerifier; +import org.keycloak.VerificationException; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.ServerRequest; +import org.keycloak.constants.ServiceAccountConstants; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +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"; + + @Override + public void init() throws ServletException { + InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json"); + KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config); + HttpClient client = new DefaultHttpClient(); + + getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment); + getServletContext().setAttribute(HttpClient.class.getName(), client); + } + + @Override + public void destroy() { + getHttpClient().getConnectionManager().shutdown(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String reqUri = req.getRequestURI(); + if (reqUri.endsWith("/login")) { + serviceAccountLogin(req); + } else if (reqUri.endsWith("/refresh")) { + refreshToken(req); + } else if (reqUri.endsWith("/logout")){ + logout(req); + } + + // Don't load products if some error happened during login,refresh or logout + if (req.getAttribute(ERROR) == null) { + loadProducts(req); + } + + req.getRequestDispatcher("/WEB-INF/page.jsp").forward(req, resp); + } + + private void serviceAccountLogin(HttpServletRequest req) { + KeycloakDeployment deployment = getKeycloakDeployment(); + HttpClient client = getHttpClient(); + + String clientId = deployment.getResourceName(); + String clientSecret = deployment.getResourceCredentials().get("secret"); + + try { + HttpPost post = new HttpPost(deployment.getTokenUrl()); + List formparams = new ArrayList(); + formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); + + String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret); + post.addHeader("Authorization", authHeader); + + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + post.setEntity(form); + + HttpResponse response = client.execute(post); + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (status != 200) { + String json = getContent(entity); + String error = "Service account login failed. Bad status: " + status + " response: " + json; + req.setAttribute(ERROR, error); + } else if (entity == null) { + req.setAttribute(ERROR, "No entity"); + } else { + String json = getContent(entity); + AccessTokenResponse tokenResp = JsonSerialization.readValue(json, AccessTokenResponse.class); + setTokens(req, deployment, tokenResp); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + req.setAttribute(ERROR, "Service account login failed. IOException occured. See server.log for details. Message is: " + ioe.getMessage()); + } catch (VerificationException vfe) { + req.setAttribute(ERROR, "Service account login failed. Failed to verify token Message is: " + vfe.getMessage()); + } + } + + private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException { + String token = tokenResponse.getToken(); + String refreshToken = tokenResponse.getRefreshToken(); + AccessToken tokenParsed = RSATokenVerifier.verifyToken(token, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + req.getSession().setAttribute(TOKEN, token); + req.getSession().setAttribute(REFRESH_TOKEN, refreshToken); + req.getSession().setAttribute(TOKEN_PARSED, tokenParsed); + } + + private void loadProducts(HttpServletRequest req) { + HttpClient client = getHttpClient(); + String token = (String) req.getSession().getAttribute(TOKEN); + + HttpGet get = new HttpGet("http://localhost:8080/database/products"); + if (token != null) { + get.addHeader("Authorization", "Bearer " + token); + } + try { + HttpResponse response = client.execute(get); + HttpEntity entity = response.getEntity(); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + String json = getContent(entity); + String error = "Failed retrieve products."; + + if (status == 401) { + error = error + " You need to login first with the service account."; + } else if (status == 403) { + error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" + + ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again."; + } + error = error + " Status: " + status + ", Response: " + json; + req.setAttribute(ERROR, error); + } else if (entity == null) { + req.setAttribute(ERROR, "No entity"); + } else { + String products = getContent(entity); + req.setAttribute(PRODUCTS, products); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + req.setAttribute(ERROR, "Failed retrieve products. IOException occured. See server.log for details. Message is: " + ioe.getMessage()); + } + } + + 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); + if (refreshToken == null) { + req.setAttribute(ERROR, "No refresh token available. Please login first"); + } else { + try { + ServerRequest.invokeLogout(deployment, refreshToken); + req.getSession().removeAttribute(TOKEN); + req.getSession().removeAttribute(REFRESH_TOKEN); + req.getSession().removeAttribute(TOKEN_PARSED); + } catch (IOException ioe) { + ioe.printStackTrace(); + req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage()); + } 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()); + } + } + } + + 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) { + + } + } + + } + + private KeycloakDeployment getKeycloakDeployment() { + return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName()); + } + + private HttpClient getHttpClient() { + return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName()); + } +} diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..9c1bac9b36 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..7eec22a6c3 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,10 @@ +{ + "realm" : "demo", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8080/auth", + "ssl-required" : "external", + "resource" : "product-sa-client", + "credentials": { + "secret": "password" + } +} \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp new file mode 100644 index 0000000000..e151f96b49 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp @@ -0,0 +1,52 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" %> +<%@ page import="org.keycloak.example.ProductServiceAccountServlet" %> +<%@ page import="org.keycloak.representations.AccessToken" %> +<%@ page import="org.keycloak.constants.ServiceAccountConstants" %> +<%@ page import="org.keycloak.util.Time" %> + + + Service account portal + + +<% + AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED); + String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS); + String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR); +%> +

Service account portal

+

Login | Refresh token | Logout

+
+ +<% if (appError != null) { %> +

+ Error: <%= appError %> +

+
+<% } %> + +<% if (token != null) { %> +

+ Service account available
+ Client ID: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID) %>
+ Client hostname: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_HOST) %>
+ Client address: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS) %>
+ Token expiration: <%= Time.toDate(token.getExpiration()) %>
+ <% if (token.isExpired()) { %> + Access token is expired. You may need to refresh
+ <% } %> +

+
+<% } %> + +<% if (products != null) { %> +

+ Products retrieved successfully from REST endpoint
+ Product list: <%= products %> +

+
+<% } %> + + + \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..5dc7103ac0 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,19 @@ + + + + service-account-portal + + + ServiceAccountExample + org.keycloak.example.ProductServiceAccountServlet + + + + ServiceAccountExample + /app/* + + + \ No newline at end of file diff --git a/examples/demo-template/service-account/src/main/webapp/index.html b/examples/demo-template/service-account/src/main/webapp/index.html new file mode 100644 index 0000000000..e2820d1744 --- /dev/null +++ b/examples/demo-template/service-account/src/main/webapp/index.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index d592010021..a26a058209 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -162,6 +162,12 @@ "publicClient": true, "directGrantsOnly": true, "consentRequired": true + }, + { + "clientId": "product-sa-client", + "enabled": true, + "secret": "password", + "serviceAccountsEnabled": true } ], "clientScopeMappings": {