();
+ 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": {