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:
- clientID/ Client secret: Keycloak demo customer portal example
- signed jwt example: Keyclock product portal example
- bearer only: keycloack demo database example
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
- New Keycloak online training - 19 janvier 2022
- Sizing Keycloak or Redhat SSO projects - 8 juin 2021
- Keycloak.X Distribution - 28 janvier 2021