1. Overview
To interact with the yuuvis® API, it is necessary to log in with a user account. The user accounts are managed by at least one external identity provider (such as Keycloak or the Active Directory Federation Service). It is possible to configure different identity providers for different tenants. Each user account belongs to exactly one tenant, which must be specified with each login.
Check out our graphical overview of the architecture which describes the basic use case flow for logging in to the core API.
2. Requirements
To work through this tutorial, the following is required:
-
Set-up yuuvis® API system (see Installation Guide)
-
A user with at least read permissions on a document type in the system (see Roles)
-
Simple Maven project
-
Preconfigured yuuvis® API system with Keycloak as the identity provider
-
Keycloak contains a realm default
-
In the realm default, the Authentication service is registered as a client under the name yuuvis
-
The realm default contains a user clouduser with the password secret
-
3. Session Management
The login is done using request headers, which are passed during the first call of an API function. The calling HTTP client must have a Cookie Manager to enrich further requests with the session cookie (GWSESSIONID). This prevents the user from logging on again for each request. In the tutorial, the OkHttpClient by Square, Inc. is used to this. Therefore, the following block must be added to the Maven dependencies in the pom.xml of the project:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-urlconnection</artifactId>
<version>3.12.0</version>
</dependency>
To activate Cookie Management in the OkHttpClient, you can do the following:
CookieJar cookieJar = new JavaNetCookieJar(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
OkHttpClient client = new OkHttpClient.Builder().cookieJar(cookieJar).build();
4. Secure Data Transmission with SSL
To use encrypted (HTTPS) endpoints with an OkHttpClient, it must be configured for the SSL encryption protocol. For this, it must also be capable of certificate treatment according to the X-509 standard. For this, the OkHttpClient receives a hostnameVerifier and a sslSocketFactory at initialization to check incoming certificates with the TLS handshake. In our example, we just keep building our X509TrustManager for the sslSocketFactory for demonstration purposes, so that certificates are almost always accepted. The following code creates an OkHttpClient that is both session- and SSL-enabled.
private OkHttpClient client = null;
// necessary to obtain access tokens via SSL
X509TrustManager trustManager = new X509TrustManager()
{
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {}
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
};
CookieJar cookieJar = new JavaNetCookieJar(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[]{this.trustManager}, new SecureRandom());
// create HTTP Client
this.client = new OkHttpClient.Builder()
.cookieJar(cookieJar)
.hostnameVerifier((s, sslSession) -> true)
.sslSocketFactory(sslContext.getSocketFactory(), this.trustManager)
.build();
5. Required System Data
To log in to the Core API, a client needs some information about the system. Of course, they first need valid credentials (username / password) of a user existing in the system. In addition, the Keycloak login client must be able to identify itself as an approved application, so it must know a registered clientId with an associated clientSecret. In addition, both pairs of credentials are only valid for a certain tenant whose name must also be known to the client for the requests. Finally, the client must also know the one accessible base URL of the Core API and the associated Keycloak identity provider.
private String userName = "clouduser";
private String userPwd = "secret";
private String userTenant = "default";
private String clientId = "clouduser";
private String clientSecret = "cd7e6ce4-781b-40db-af5f-e4106926e96c"; //client secret, obtainable through keycloak credentials tab of client matching client id and tenant
private String yuuvisBaseUrl= "http://10.10.6.242"; //Base URL of yuuvisclient
private String keycloakBaseUrl = "https://10.10.6.252:8443"; //Base URL of keycloak identity provider
5.1. Generating a Secret
To log in with an oAuth2 access token, the client must authenticate itself to Keycloak with its client secret. This secret is static and can be taken from the Keycloak interface. To do this, log on to the Keycloak interface with a technical user, select the tenant applicable to the client, select the correct client under "Clients" (here "Enaio") and take out the secret of the client under the "Credentials" tab.
6. Login Procedures
6.1. Login with Username and Password
The username, password, and associated tenant are passed as HTTP headers. The username and password must be encoded with Base64 in the same way as for classic Basic authentication. The headers are then sent to any Core API endpoint.
byte[] credentials = "clouduser:secret".getBytes(StandardCharsets.UTF_8);
String authorization = "Basic " + Base64.getEncoder().encodeToString(credentials);
Headers headers = new Headers.Builder()
.add("Authorization", authorization)
.add("X-ID-TENANT-NAME", "default")
.build();
6.2. Login with an OAuth 2.0 Access Token
For this login procedure, the application must have received an access token from the identity provider from a third-party application (for example, a proxy). The access token and the associated tenant are passed as HTTP headers for login.
Keycloak can request an access token with the Password Credentials Grant Flow. The request must contain the user name, the user password, the client ID and the secret as parameters. Keycloak’s response contains a JSON object from which the access token and token type must be extracted. In the example, JsonPath is used for this.
String payload = "client_id=enaio&" +
"client_secret=4c5254363c1d&" +
"username=clouduser&" +
"password=secret&" +
"grant_type=password";
//retrieve access token from identity provider (Keycloak)
Request.Builder request = new Request.Builder()
.url(keycloakBaseUrl+"/auth/realms/default/protocol/openid-connect/token")
.post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), payload))
.build();
String responseJson = this.client.newCall(request).execute().body().string();
DocumentContext context = JsonPath.parse(responseJson);
String tokenType = context.read("token_type");
String accessToken = context.read("access_token");
Finally, the logon headers are created and sent to any Core API endpoint.
Headers headers = new Headers.Builder()
.add("Authorization", tokenType + " " + accessToken)
.add("X-ID-TENANT-NAME", "default")
.build();
6.3. Login with the OAuth 2.0 Device Flow
With this logon procedure, the client application calls the system browser so that the user can log on to an HTML form. The OAuth 2.0 Device Flow is used for this purpose. The procedure is done in the following four steps:
-
Start the login process and read out the parameters Device Code, User Code, and Verification URI. The example uses JsonPath for this. The parameter values lose their validity after five minutes. In this time window, the user must have finished logging in to the browser.
OAuth 2.0 Device Flow: Step 1Request.Builder startRequest = new Request.Builder().url(keycloakBaseUrl+"/tenant/default/loginDevice"); String responseJson = this.client.newCall(startRequest.build()).execute().body().string(); DocumentContext context = JsonPath.parse(responseJson); String deviceCode = context.read("device_code"); String userCode = context.read("user_code"); String verificationUri = context.read("verification_uri");
-
The Verification URI is opened with the User Code as a parameter in the system browser. The Authentication service then forwards to the login page of the identity provider, where the user can log in.
OAuth 2.0 Device Flow: Step 2Desktop.getDesktop().browse(new URI(keycloakBaseUrl + verificationUri + "?user_code=" + userCode));
-
At regular intervals, the client application queries the Authentication service to see whether the user has successfully logged in. This is the case if the status request receives the return code OK (200). The response of the status request contains a JSON object from which the access token and the token type must be extracted. In the example, JsonPath is used for this.
OAuth 2.0 Device Flow: Step 3String tokenType = null; String accessToken = null; for (int i = 1 ; i != 30 ; i++, Thread.sleep(2000)) { Request.Builder pollingRequest = new Request.Builder().url(keycloakBaseUrl+"/auth/info/state?device_code=" + deviceCode); Response pollingResponse = this.client.newCall(pollingRequest.build()).execute(); if (pollingResponse.code() != 200) continue; context = JsonPath.parse(pollingResponse.body().string()); tokenType = context.read("token_type"); accessToken = context.read("access_token"); break; }
-
Finally, the logon headers are assembled and sent to any Core API endpoint.
OAuth 2.0 Device Flow: Step 4Headers headers = new Headers.Builder() .add("Authorization", tokenType + " " + accessToken) .add("X-ID-TENANT-NAME", "default") .build();
7. Logout
After all necessary API operations have been performed, the session established by the logon can be closed.
Request.Builder logoutRequest = new Request.Builder().url(keycloakBaseUrl+"/logout");
this.client.newCall(logoutRequest.build()).execute();
8. Summary
This tutorial explains how an OkHttp3 Java client implements the different logon procedures for the Core API.