KEYCLOAK-401 Added service account demo
This commit is contained in:
parent
d8c1081578
commit
1a34feda65
10 changed files with 404 additions and 0 deletions
|
@ -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
|
||||
==========================
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<module>database-service</module>
|
||||
<module>third-party</module>
|
||||
<module>third-party-cdi</module>
|
||||
<module>service-account</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
|
60
examples/demo-template/service-account/pom.xml
Normal file
60
examples/demo-template/service-account/pom.xml
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-examples-demo-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.4.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.keycloak.example.demo</groupId>
|
||||
<artifactId>service-account-example</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Service Account Example App</name>
|
||||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-adapter-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>service-account-portal</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jboss.as.plugins</groupId>
|
||||
<artifactId>jboss-as-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.wildfly.plugins</groupId>
|
||||
<artifactId>wildfly-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<dependencies>
|
||||
<!-- the Demo code uses classes in these modules. These are optional to import if you are not using
|
||||
Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
|
||||
<module name="org.apache.httpcomponents"/>
|
||||
</dependencies>
|
||||
</deployment>
|
||||
</jboss-deployment-structure>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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" %>
|
||||
<html>
|
||||
<head>
|
||||
<title>Service account portal</title>
|
||||
</head>
|
||||
<body bgcolor="#FFFFFF">
|
||||
<%
|
||||
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
|
||||
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
|
||||
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
|
||||
%>
|
||||
<h1>Service account portal</h1>
|
||||
<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a
|
||||
href="/service-account-portal/app/logout">Logout</a></p>
|
||||
<hr />
|
||||
|
||||
<% if (appError != null) { %>
|
||||
<p><font color="red">
|
||||
<b>Error: </b> <%= appError %>
|
||||
</font></p>
|
||||
<hr />
|
||||
<% } %>
|
||||
|
||||
<% if (token != null) { %>
|
||||
<p>
|
||||
<b>Service account available</b><br />
|
||||
Client ID: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID) %><br />
|
||||
Client hostname: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_HOST) %><br />
|
||||
Client address: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS) %><br />
|
||||
Token expiration: <%= Time.toDate(token.getExpiration()) %><br />
|
||||
<% if (token.isExpired()) { %>
|
||||
<font color="red">Access token is expired. You may need to refresh</font><br />
|
||||
<% } %>
|
||||
</p>
|
||||
<hr />
|
||||
<% } %>
|
||||
|
||||
<% if (products != null) { %>
|
||||
<p>
|
||||
<b>Products retrieved successfully from REST endpoint</b><br />
|
||||
Product list: <%= products %>
|
||||
</p>
|
||||
<hr />
|
||||
<% } %>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<module-name>service-account-portal</module-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>ServiceAccountExample</servlet-name>
|
||||
<servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>ServiceAccountExample</servlet-name>
|
||||
<url-pattern>/app/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
|
@ -0,0 +1,5 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Refresh" content="0; URL=app">
|
||||
</head>
|
||||
</html>
|
|
@ -162,6 +162,12 @@
|
|||
"publicClient": true,
|
||||
"directGrantsOnly": true,
|
||||
"consentRequired": true
|
||||
},
|
||||
{
|
||||
"clientId": "product-sa-client",
|
||||
"enabled": true,
|
||||
"secret": "password",
|
||||
"serviceAccountsEnabled": true
|
||||
}
|
||||
],
|
||||
"clientScopeMappings": {
|
||||
|
|
Loading…
Reference in a new issue