In today’s post I will describe the process of integrating OIDC implicit flow with ORDS running on Tomcat against Keycloak service. May sound complicated, but we’ll break it down into individual components so we know what we’re talking about.
OIDC implicit flow
OIDC or OpenID Connect is an extension of OAuth 2.0. This may sound familiar for people who were dealing before with ORDS built-in authentication methods, since one of them, the client credentials flow, is based on the same protocol (for more information on this see this nice blog entry from JMJ Cloud: https://www.jmjcloud.com/blog/ords-securing-services-using-oauth2-2-legged).
We’ll be using the implicit flow, which has generally 4 steps for authentication:
- The user is redirected to the authentication server by the application
- The user puts in his credentials
- The user is redirected back to the application with an access token in the URL fragment
- All the following request to the application are made with “Authorization: Bearer …” header
In our case it will be up to the application server (Tomcat) to verify the provided token.
If you want to know more about this flow, it’s described very clearly in this OneLogin documentation entry: https://developers.onelogin.com/openid-connect#Implicit-Flow
As you can see, the process is pretty straightforward and that’s why we’ll be using it for this demo.
Keycloak
Keycloak is an open source, Red Hat-backed Identity and Access Management server. The part we’ll be interested in is its OpenID Connect adapter for Tomcat: https://www.keycloak.org/docs/latest/securing_apps/#_tomcat_adapter but there’s a wide variety of them, covering all your possible needs.
What would normally be required is to put a security-constraint entry in the web.xml of the application, effectively protecting the whole ORDS application. But we certainly don’t want to do that. We want to use the ORDS’ built-in endpoint security management where we list the privileges necessary to access selected endpoints. In SQL Developer this may look something like this:
We may want to leave some endpoints open to everyone and certainly we want to keep the metadata-catalog endpoint public as well.
In order to achieve our goal, we will create a small Tomcat valve that is going to check every request and if there’s “Authorization: Bearer …” header in that request, it will trigger the token validation, which is implemented in the Keycloak adapter.
This adapter populates the user principal, which then ORDS can access and use for the privilege mappings. It will also allow us to use the REMOTE_USER CGI variable if we want to have access e.g. to the login of the user from within ORDS.
Let’s take a look at a fragment of the code:
public class OrdsOidcAuthValve extends org.keycloak.adapters.tomcat.KeycloakAuthenticatorValv{ @Override public void invoke(Request request, Response response) throws IOException, ServletException { if(request.getHeader("authorization") != null && request.getHeader("authorization").contains("Bearer")){ checkToken(request,response); } super.invoke(request,response); } private AuthOutcome checkToken(Request request, Response response){ CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response); KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade); if (deployment != null && deployment.isConfigured()) { AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment); this.nodesRegistrationManagement.tryRegister(deployment); CatalinaRequestAuthenticator authenticator = this.createRequestAuthenticator(request, facade, deployment, tokenStore); AuthOutcome outcome = authenticator.authenticate(); return outcome; } return null; } }
As you can see, the logic is simple yet brilliant and could be adjusted for other flows as well. It would be nice of course to have this possibility somehow integrated into the ORDS itself and avoid creating these customization, but in the current state that is not the case and this is the only way I see to integrate with your current SSO.
With this valve in place we don’t have to worry about anything else. We just protect our endpoint like we would normally, first defining a role that will match with the one received from our SSO service, then role-privilege mapping and finally we protect the endpoint, all using the ords package. Something like this:
DECLARE l_priv_roles owa.vc_arr; l_priv_patterns owa.vc_arr; l_priv_modules owa.vc_arr; BEGIN l_priv_roles(1) := 'Tickets User'; l_priv_patterns(1) := '/my/*'; l_priv_patterns(2) := '/comments/*'; l_priv_modules(1) := 'my.tickets'; ords.create_role('Tickets User'); ords.define_privilege( p_privilege_name => 'tickets.privilege', p_roles => l_priv_roles, p_patterns => l_priv_patterns, P_modules => l_priv_modules, p_label => 'Task Ticketing Access', p_description => 'Provides the ability to create, ' || 'update and delete tickets ' || 'and post comments on tickets' ); END; /
When we make a request with the proper header:
curl --location --request GET ‘https://localhost:8443/api/v1/employees' \ --header 'Authorization: Bearer <<token>>
everything should work perfectly and we would gain access to our protected endpoint.
I hope you find this entry helpful. If you are interested in SSO integration for Tomcat, you can check out our GitHub repo: https://github.com/cerndb/tomcat-sso-integration-components with some useful valves (kudos to Luis!).