Oauth2/Openid client authentication methods with Redhat SSO : this article explores the Oauth2/openID confidential client authentication methods, and brings some insights using Redhat-SSO example.

1) Public Client, Confidential Client

There are 2 types of clients: public client and confidential clients

  • Public Client:
    Clients incapable of maintaining the confidentiality of their credentials They are also incapable of secure client authentication
  • Confidential client:
    Clients capable of secure client authentication and able to maintain the confidentiality of their credentials

For more details, see RFC 6749 section 2.1

2) Oauth2/openID client access type with RedHat SSO

There 3 possible types of access:

  • public: does not require a secret
  • confidential: client requires a secret to initiate the login protocol
  • bearer-only: client are web services that never initiates a login (example: an application connnecting to a database)

The bearer-only type is special  kind of confidential client with no login, and is for example used for an application to connect to a database.

On the remaining of this article, the focus is brought on confidential clients and bearer-only to perform secure authentication
The client access type has to be at first configured with  « confidential » or « bearer-only ».  Within RedHat SSO, once teh acccess type is configured, another client TAB appears on the top left menu called « credentials ».

Confidential clients also comes with 2 different ways of dealing with:

  • client_id/client_secret
  • signed JWT

In the remaining of this article, the 3 followings aspects are illustrated with keycloak examples:

3) Confidential  – client_id/ client_secret

This illustrated with the example demo/customer-portal. It is based

3.1) RH-SSO configuration for client secret

Client access type: Confidential

RH-SSO client is configured with

client_id: customer-portal
client_secret: 0073ccab-b649-4aea-bb53-98f9ae5b6481
3.2) client application -keycloak.json

The client application (customer-portal) is updated using keycloak.json

keycloak.json customer-portal

{
"realm": "demo",
"resource": "customer-portal",
"auth-server-url": "https://localhost:8180/auth",
"ssl-required" : "external",
"expose-token": true,
"credentials": {
"secret": "password"
}
}
3.3) web.xml
<login-config>
  <auth-method>KEYCLOAK</auth-method>
  <realm-name>demo</realm-name>
</login-config>


<security-role>
   <role-name>admin</role-name>
</security-role>

<security-role>
   <role-name>user</role-name>
</security-role>
3.4) standalone.xml

The following has also to be added to Jboss application Server standalone/configuration/standalone.xml

<secure-deployment name="customer-portal.war">
<realm>demo</realm>
<resource>customer-portal</resource>
<auth-server-url>https://localhost:8180/auth</auth-server-url>
<credential name="secret">0073ccab-b649-4aea-bb53-98f9ae5b6481</credential>
</secure-deployment>
3.5) tracing inner calls

Tracing internally with the SAML tracer firefox plugin, it is possible to follow the openID request

The GET request is issued when  getting the screen where to authentify to

GET
https://localhost:8180/auth/realms/demo/protocol/openid-connect/auth?
response_type=code&
client_id=customer-portal&
redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcustomer-portal%2Fcustomers%2Fview.jsp&
state=1cd48b6e-7f8a-4b51-8c3e-ae0bac235ff3&
login=true&
scope=openid HTTP/1.1
The Post is obtained after having performed authentication


POST https://localhost:8180/auth/realms/demo/login-actions/authenticate?code=UkBhS8sde7wuxErhHKvZL_wymDTOtwFKlgFr3UKCQ5Y.28eda7b3-176f-494d-8f3a-6f0ecc595dcf&execution=7ba6a5b6-a724-4039-8683-5a2cff8b9405 HTTP/1.1
Host: localhost:8180
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: https://localhost:8180/auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=customer-portal&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcustomer-portal%2Fcustomers%2Fview.jsp&state=1cd48b6e-7f8a-4b51-8c3e-ae0bac235ff3&login=true&scope=openid
Content-Type: application/x-www-form-urlencoded
Content-Length: 59
Cookie: KC_RESTART=eyJhbGciOiJIUzI1NiIsImtpZCIgOiAiODQ4MzcyZDktN2Q5ZC00NDk2LWI3ODUtYWU4NWYzNzZjNjk1In0.eyJjcyI6IjI4ZWRhN2IzLTE3NmYtNDk0ZC04ZjNhLTZmMGVjYzU5NWRjZiIsImNpZCI6ImN1c3RvbWVyLXBvcnRhbCIsInB0eSI6Im9wZW5pZC1jb25uZWN0IiwicnVyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9jdXN0b21lci1wb3J0YWwvY3VzdG9tZXJzL3ZpZXcuanNwIiwiYWN0IjoiQVVUSEVOVElDQVRFIiwibm90ZXMiOnsiYXV0aF90eXBlIjoiY29kZSIsInNjb3BlIjoib3BlbmlkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MTgwL2F1dGgvcmVhbG1zL2RlbW8iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9jdXN0b21lci1wb3J0YWwvY3VzdG9tZXJzL3ZpZXcuanNwIiwic3RhdGUiOiIxY2Q0OGI2ZS03ZjhhLTRiNTEtOGMzZS1hZTBiYWMyMzVmZjMiLCJjbGllbnRfcmVxdWVzdF9wYXJhbV9sb2dpbiI6InRydWUifX0.ncRfqVdFpsMfRozcj_2mXJGFbXR5D1e53M3CjHGbef
HTTP/?.? 302 Found
Expires: 0
Cache-Control: no-cache, no-store, must-revalidate
X-Powered-By: Undertow/1
Set-Cookie: JSESSIONID=ZH-tkZ5hM9mC0H9sa3bpYGk8LBZxU1Nrnuhq6bLm.asus; path=/customer-portal
OAuth_Token_Request_State=; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:00 GMT
Server: JBoss-EAP/7
Pragma: no-cache
Location: https://localhost:8080/customer-portal/customers/view.jsp
Date: Mon, 02 Oct 2017 15:23:53 GMT
Connection: keep-alive
Content-Length: 0
3.6 deciphering ID_token
header
{
"alg": "HS256",
"kid": "848372d9-7d9d-4496-b785-ae85f376c695"
}
Payload
{
"cs": "28eda7b3-176f-494d-8f3a-6f0ecc595dcf",
"cid": "customer-portal",
"pty": "openid-connect",
"ruri": "https://localhost:8080/customer-portal/customers/view.jsp",
"act": "AUTHENTICATE",
"notes": {
"auth_type": "code",
"scope": "openid",
"iss": "https://localhost:8180/auth/realms/demo",
"response_type": "code",
"redirect_uri": "https://localhost:8080/customer-portal/customers/view.jsp",
"state": "1cd48b6e-7f8a-4b51-8c3e-ae0bac235ff3",
"client_request_param_login": "true"
}
}
4) Confidential client – signed jwt

When dealing with client-signed jwt, you have to deal pick USE_JWKS URL selection.

When USE_JWKS is enabled, it means that RH-SSO server will directly read the client application public key (if public key are used) from the customer keystore URL.

When USE_JWKS is disabled, it means that client application  public key has to be directly imported in RH-SSO public key store

Using a client application USE_JWKS enabled keystore allows to easily to rotate customer application keys as there is no need to import new keys within RH-SSO server.  Once client application keys are updated, RH-SSO detects that the client kid  header has changed and will it reimport a new public key.

This is not the case, when USE_JWKS is disabled, You need to update the RH-SSO with the new key of your client application.

4.1) RH-SSO Client configuration
  • Client access type: Confidential
  • Client Authenticator: signed JWT
  • Use_JWKS URL: ON
    JWKS URL: https://localhost:8080/product-portal/k_jwks
4.2) client application keycloak.json
{
"realm" : "demo",
"resource" : "product-portal",
"auth-server-url" : "https://localhost:8180/auth",
"ssl-required" : "external",
"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
}
}
}
4.3) web.xml
<login-config>
  <auth-method>KEYCLOAK</auth-method>
  <realm-name>demo</realm-name>
</login-config>


<security-role>
   <role-name>admin</role-name>
</security-role>

<security-role>
   <role-name>user</role-name>
</security-role>
4.4) standalone.xml
 <secure-deployment name="product-portal.war">
 <realm>demo</realm>
 <resource>product-portal</resource>
 <auth-server-url>https://localhost:8180/auth</auth-server-url>
 <credential name="jwt">
 <client-key-password>keypass</client-key-password>
 <client-keystore-file>classpath:keystore-client.jks</client-keystore-file>
 <client-keystore-password>storepass</client-keystore-password>
 <client-key-alias>clientkey</client-key-alias>
 <token-expiration>10</token-expiration>
 <client-keystore-type>JKS</client-keystore-type>
 </credential>
</secure-deployment>

 

5) Jwt bearer-only

This case implies that there is no login screen, and is used for web services to connect to an application.

5.1) RH-SSO Configuration
  • client access type: bearer only
  • Client authentificator: signed JWT
  • No Client ceertificate configured
5.2) client application – keycloack.json
{
"realm" : "demo",
"resource" : "database-service",
"auth-server-url": "https://localhost:8180/auth",
"bearer-only" : true,
"ssl-required" : "external"
}
5.3) web.xml
<login-config>
  <auth-method>KEYCLOAK</auth-method>
  <realm-name>demo</realm-name>
</login-config>


<security-role>
   <role-name>admin</role-name>
</security-role>

<security-role>
   <role-name>user</role-name>
</security-role>
5.4) standalone/configuration/standalone.xml
<secure-deployment name="database.war">
     <realm>demo</realm>
        <auth-server-url>https://localhost:8180/auth</auth-server-url>
         <resource>database-service</resource>
           <bearer-only>true</bearer-only>
 </secure-deployment>
5.5) building a signed JWT using java code

In this file, authentication  to the database is done using  JWT bearer Authentication.

It is done in 2 steps:

  • a JWT token is retrieved session.getTokenString()
  • the authorisation bearer tag is added to the header
    get.addHeader("Authorization", "Bearer " + session.getTokenString());

    The following piece of code illustrates how to get JWT to query the database

public static List getProducts(HttpServletRequest req) throws Failure {
KeycloakSecurityContext session = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());

HttpClient client = new DefaultHttpClient();
try {
HttpGet get = new HttpGet(UriUtils.getOrigin(req.getRequestURL().toString()) + "/database/products");
get.addHeader("Authorization", "Bearer " + session.getTokenString());
try {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != 200) {
throw new Failure(response.getStatusLine().getStatusCode());
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
try {
return JsonSerialization.readValue(is, TypedList.class);
} finally {
is.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
} finally {
client.getConnectionManager().shutdown();
}
}
5.6) Retrieving the values from a JSP

The Product list is displayed as follows:

<%
java.util.List list = null;
try {
list = ProductDatabaseClient.getProducts(request);
} catch (ProductDatabaseClient.Failure failure) {
out.println("There was a failure processing request. You either didn't configure Keycloak properly, or maybe" +
"you just forgot to secure the database service?");
out.println("Status from database service invocation was: " + failure.getStatus());
return;
}
for (String cust : list)
{
out.print("");
out.print(cust);
out.println("");}
%>

6) pointers

RFC 6749  The OAuth 2.0 Authorization Framework
RFC 6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage
RFC 7515 JSON Web Signature (JWS)
RFC 7516 JSON Web Encryption (JWE)
RFC 7519 JSON Web Token (JWT)
RFC 7523  JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants

janua
Les derniers articles par janua (tout voir)