OpenID Connect (OIDC) is an authentication protocol using a third-party site to provide single-sign-on capabilities to web applications. For example, before you can visualize OpenSDU data in IVAAP, your browser is redirected to a login page hosted by Microsoft or Amazon. After completion of the two-factor authentication, your browser is redirected to IVAAP. The sequence of steps between IVAAP and external authentication servers follows the OpenID Connect protocol.
OIDC is built on top of Oauth2. It was created in 2014, superseding Open ID 2.0. Over the years, it has become a widely used standard for consumer and enterprise-grade applications. While simpler than OpenID 2.0, most developers will need to use a library to integrate OIDC with their applications.
In IVAAP’s case, since the Admin Backend is written in Java, we use the OIDC Nimbus library identified by this maven configuration:
<dependency> <groupId>com.nimbusds</groupId> <artifactId>oauth2-oidc-sdk</artifactId> <version>10.1</version> </dependency>
While this Nimbus library doesn’t provide a full implementation of the OpenID Connect workflow, it contains pieces that can be reused in your code. IVAAP being a platform, it comes with an SDK to plug your own authentication. Nimbus fits the concept of an SDK where most of the work is already done for developers, and you only need to implement a few hooks. In our case, the hooks identify mainly what to do when the /login, /callback, /refresh, and /logout services are called. Let’s dive a little further into how Nimbus helps developers implement these services.
The Login Service
The main purpose of the /login service is to redirect the browser to an external authentication page. The login URL changes slightly each time it’s called for security reasons. It contains various information such as the scope, a callback URL, a client ID, a state, and a nonce. The scope, callback URL, and client ID typically don’t change, but the state and nonce are always new.
ClientID clientID = new ClientID(this.clientId); URI callback = new URI(this.callbackURL); Nonce nonce = new Nonce(); Scope authScope = new Scope(); String[] split = this.scope.split(" "); for (String currentToken : split) { authScope.add(currentToken); } AuthenticationRequest request = new AuthenticationRequest.Builder( ResponseType.CODE, authScope, clientID, callback) .endpointURI(new URI(this.authURL)) .state(state) .nonce(nonce) .prompt(new Prompt("login")) .build(); return request.toURI();
Since OIDC was released, a more secure variation called PKCE (pronounced pixy) has been added. PKCE introduces a code challenge instead of relying on a client secret used in the /callback service. The same code looks like this when PKCE is used:
ClientID clientID = new ClientID(this.clientId); URI callback = new URI(this.callbackURL); this.codeVerifier = new CodeVerifier(); Nonce nonce = new Nonce(); Scope authScope = new Scope(); String[] split = this.scope.split(" "); for (String currentToken : split) { authScope.add(currentToken); } AuthenticationRequest request = new AuthenticationRequest.Builder( ResponseType.CODE, authScope, clientID, callback) .endpointURI(new URI(this.authURL)) .state(state) .codeChallenge(codeVerifier, CodeChallengeMethod.S256) .nonce(nonce) .prompt(new Prompt("login")) .build(); return request.toURI();
The Callback Service
When the /callback service is called after successful authentication, the callback URL contains a state and a code that identifies the authentication that was just performed. The following lines extract this code from the callback URL:
AuthenticationResponse response = AuthenticationResponseParser.parse( new URI(callbackUrl)); State state = response.getState(); AuthorizationCode code = response.toSuccessResponse().getAuthorizationCode();
The state should match the state created when the /login service was called.
This “authorization code” can be exchanged with authentication tokens calling the OIDC token service.
URI tokenEndpoint = new URI(this.tokenURL); URI callback = new URI(this.callbackURL); AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback); ClientID clientID = new ClientID(this.clientId); Secret secret = new Secret(this.clientSecret); Scope authScope = new Scope(); String[] split = this.scope.split(" "); for (String currentToken : split) { authScope.add(currentToken); } ClientAuthentication clientAuth = new ClientSecretBasic(clientID, secret); TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, authScope); HTTPRequest toHTTPRequest = request.toHTTPRequest(); TokenResponse tokenResponse= OIDCTokenResponseParser.parse(toHTTPRequest.send()); OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse(); JWT idToken = successResponse.getOIDCTokens().getIDToken(); AccessToken accessToken = successResponse.getOIDCTokens().getAccessToken(); RefreshToken refreshToken = successResponse.getOIDCTokens().getRefreshToken();
If PKCE is enabled, this code is simpler. It doesn’t require a client’s secret to be passed:
URI tokenEndpoint = new URI(this.tokenURL); URI callback = new URI(this.callbackURL); AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, this.codeVerifier); ClientID clientID = new ClientID(this.clientId); TokenRequest request = new TokenRequest(tokenEndpoint, clientID, codeGrant); authScope); HTTPRequest toHTTPRequest = request.toHTTPRequest(); TokenResponse tokenResponse= OIDCTokenResponseParser.parse(toHTTPRequest.send()); OIDCTokenResponse successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse(); JWT idToken = successResponse.getOIDCTokens().getIDToken(); AccessToken accessToken = successResponse.getOIDCTokens().getAccessToken(); RefreshToken refreshToken = successResponse.getOIDCTokens().getRefreshToken();
The OpenID Connect token service gives us 3 tokens:
- An access token
- A JSON Web Token (JWT) token, also known as an ID token or bearer token
- A refresh token
The access token is the token that typically grants access to data. It expires, and a new access token can be retrieved, passing the refresh token to the OIDC refresh service.
The JWT token is the token that identifies the user. Unlike the access token, it doesn’t expire. While a JWT token may be parsed to get user info, the access token is typically used instead. A user info OIDC service typically needs to be called with the access token to get the user details.
URI userInfoURI = new URI(this.userInfoURL); HTTPResponse httpResponse = new UserInfoRequest(userInfoURI, accessToken) .toHTTPRequest() .send(); UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse); UserInfo userInfo = userInfoResponse.toSuccessResponse().getUserInfo(); String email = (String) userInfo.getClaim("email");
For OpenSDU, a more complex API involving claim verifiers needs to be used to get user details.
Set<String> claims = new LinkedHashSet<>(); claims.add("unique_name"); ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWT"))); JWKSource<SecurityContext> keySource = new RemoteJWKSet<>(...)); JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256; JWSKeySelector<SecurityContext> keySelector = new JWSVerificationKeySelector<>(expectedJWSAlg, keySource); jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier(new JWTClaimsSet.Builder().issuer(...).build(), claims)); jwtProcessor.setJWSKeySelector(keySelector); JWTClaimsSet claimsSet = jwtProcessor.process(accessToken.getValue(), null); Map<String, Object> userInfo = claimsSet.toJSONObject(); String email = (String) userInfo.getClaim("unique_name");
The Refresh Service
In IVAAP’s case, the UI’s role is to call the /refresh service before an access token expires. When this refresh service is called with the last issued refresh token, new access and refresh tokens are obtained by calling the OIDC token service again.
RefreshToken receivedRefreshToken = new RefreshToken(…); AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(receivedRefreshToken); URI tokenEndpoint = new URI(this.tokenURL); ClientID clientID = new ClientID(this.clientId); Secret secret = new Secret(this.clientSecret); ClientAuthentication clientAuth = new ClientSecretBasic(clientID, secret); Scope authScope = new Scope(); String[] split = this.scope.split(" "); for (String currentToken : split) { authScope.add(currentToken); } TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, refreshTokenGrant, authScope); HTTPResponse httpResponse = request.toHTTPRequest().send(); AccessTokenResponse successResponse = response.toSuccessResponse(); Tokens tokens = successResponse.getTokens(); AccessToken accessToken = tokens.getAccessToken(); RefreshToken refreshToken = tokens.getRefreshToken();
If PKCE is enabled, this code is simpler. It doesn’t require a client secret to be passed:
RefreshToken receivedRefreshToken = new RefreshToken(…); AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(receivedRefreshToken); URI tokenEndpoint = new URI(this.tokenURL); ClientID clientID = new ClientID(this.clientId); TokenRequest request = new TokenRequest(tokenEndpoint, clientID, refreshTokenGrant); HTTPResponse httpResponse = request.toHTTPRequest().send(); AccessTokenResponse successResponse = response.toSuccessResponse(); Tokens tokens = successResponse.getTokens(); AccessToken accessToken = tokens.getAccessToken(); RefreshToken refreshToken = tokens.getRefreshToken();
The Logout Service
As OpenID Connect doesn’t provide a standard for logging out, no Nimbus API generates the logout URL. The logout URL has to be built manually depending on the OpenID provider (Microsoft or Amazon). This logout URL is sometimes provided by the content behind the discovery URL of an OpenID provider.
Going Beyond
For simplicity’s sake, I didn’t include in the code samples the handling of errors. While the OpenID Connect protocol is well-defined, there are a few variations between cloud providers. For example, the fields stored in tokens may vary. This article doesn’t describe what happens after the /callback service is called: once tokens are issued, how are they passed to the viewer? These implementation details may be implemented differently by each application. When I was tasked with integrating OpenID Connect, I found the Nimbus website clear and simple to use, showing sample code front and center. I highly recommend this library.
Visit us online at int.com/ivaap for a preview of IVAAP or for a demo of INT’s other data visualization products.
For more information, please visit www.int.com or contact us at intinfo@int.com.
____________
ABOUT INT
INT software empowers energy companies to visualize their complex data (geoscience, well, surface reservoir, equipment in 2D/3D). INT offers a visualization platform (IVAAP) and libraries (GeoToolkit) that developers can use with their data ecosystem to deliver subsurface solutions (Exploration, Drilling, Production). INT’s powerful HTML5/JavaScript technology can be used for data aggregation, API services, and high-performance visualization of G&G and energy data in a browser. INT simplifies complex subsurface data visualization.
INT, the INT logo, and IVAAP are trademarks of Interactive Network Technologies, Inc., in the United States and/or other countries.