Writing an OIDC RP Utilities Class

 Recently, I found the need to write an OIDC RP utility class in java in order to handle OIDC requests. I needed to be able to call the usual suspects of when dealing with OIDC:

  1. Authorize
  2. Token
  3. Userinfo
Each one of these is a different API call and I really didn't feel like writing individual requests and deal with processing and storing all of the information that would be exchanged. The obvious first thought would then be to look for a Java library that is already doing this sort of thing and then wrap it in an easy to use utility for my codebase.

Thus the search for the optimal utility began. Being that OIDC was a published standard I figured that they would have a list of libraries that they supported and that is when I came across their certified library list.


One thing that I did know was that my particular use case required that the library support Identity Proofing, which is being implemented in the OIDC standard. This narrowed my list down to what I decided was the winner, a library called Nimbus OAuth 2.0 SDK with OpenID Connect extensions by Connect2id


Not only did this library support all my needs, but it also provided many examples that I could follow to best integrate. So I imported the library and got to work.

The first thing that I needed was to be able to store all of the OIDC configurations in an easy and reusable asset, which I did by creating a metadata creator method.

public static OIDCProviderMetadata getOIDCProviderMetadata(String issuer) throws IOException, ParseException {
OIDCProviderMetadata oidcProviderMetadata = null;
Issuer iss = new Issuer(issuer);
try {
oidcProviderMetadata = OIDCProviderMetadata.resolve(iss);
} catch (Exception e) {
// Process your exception here
}

if (oidcProviderMetadata == null){
String wellKnownUri = issuer + "/.well-known/openid-configuration";
JSONObject wellKnownConfig = getWellKnownConfig(URI.create(wellKnownUri));
Issuer iss2 = new Issuer(wellKnownConfig.getAsString("issuer"));
List<SubjectType> st = new ArrayList<>();
List<String> strl = (List<String>) wellKnownConfig.getOrDefault("subject_types_supported", Arrays.asList("public"));
for(String s : strl){
st.add(SubjectType.parse(s));
}
URI jwksetUri = URI.create(wellKnownConfig.getAsString("jwks_uri"));
oidcProviderMetadata = new OIDCProviderMetadata(iss2,st,jwksetUri);
oidcProviderMetadata.setAuthorizationEndpointURI(URI.create(wellKnownConfig.getAsString("authorization_endpoint")));
oidcProviderMetadata.setTokenEndpointURI(URI.create(wellKnownConfig.getAsString("token_endpoint")));
oidcProviderMetadata.setUserInfoEndpointURI(URI.create(wellKnownConfig.getAsString("userinfo_endpoint")));
}

return oidcProviderMetadata;
}

This would get all the metadata that is configured by the provider at their well know endpoint. This doesn't account for OIDC vendors that don't have a well known endpoint, but will come back to that.

From here I wrote a method to construct the authorization endpoint with all the parameters I would be sending along to the user

public static String getAuthorizationUri(URI authEndpoint, String clientId, URI redirectUri, String state) {
final String METHOD="getAuthorizationUri";
String uri = "";
State stateObj = new State(state);
Nonce nonce = new Nonce();

AuthenticationRequest request = new AuthenticationRequest.Builder(
new ResponseType(ResponseType.Value.CODE),
new Scope(OIDCScopeValue.OPENID),
new ClientID(clientId),
redirectUri)
.endpointURI(authEndpoint)
.state(stateObj)
.nonce(nonce)
.build();
uri = request.toURI().toString();
return uri;
}

This would redirect the user to the authorization endpoint to complete the request and callback to the service.
After that I would need to call the token endpoint

public static OIDCTokenResponse getOIDCTokens(String code, URI tokenEndpoint, String clientId, String secret, URI redirectUri) throws IOException, ParseException {
final String METHOD="getOIDCTokens";
OIDCTokenResponse successResponse = null;

AuthorizationCode authcode = new AuthorizationCode(code);
AuthorizationGrant codeGrant = new AuthorizationCodeGrant(authcode, redirectUri);
// The credentials to authenticate the client at the token endpoint
ClientID cid = new ClientID(clientId);
Secret clientSecret = new Secret(secret);
ClientAuthentication clientAuth = new ClientSecretBasic(cid, clientSecret);

// Make the token request
TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, codeGrant);
TokenResponse tokenResponse = OIDCTokenResponseParser.parse(request.toHTTPRequest().send());
if (!tokenResponse.indicatesSuccess()) {
// We got an error response...
TokenErrorResponse errorResponse = tokenResponse.toErrorResponse();
}
successResponse = (OIDCTokenResponse) tokenResponse.toSuccessResponse();

return successResponse;
}

This gave me the identity token that I could use.
Then all that is left is the Userinfo endpoint

public static UserInfoResponse getUserInfo(URI userInfoEndpoint, BearerAccessToken token) throws IOException, ParseException {
final String METHOD="getUserInfo";
UserInfo userInfo = null;
UserInfoResponse userInfoResponse = null;

HTTPRequest req = new UserInfoRequest(userInfoEndpoint, token).toHTTPRequest();
// Make the request
req.setHeader("Content-Type","application/json");
req.setHeader("Accept","application/json");
HTTPResponse httpResponse = req.send();

// Parse the response
try {
JSONObject content = httpResponse.getContentAsJSONObject();
if(httpResponse.getStatusCode()==200 && content.containsKey("error") && !"".equals(content.getAsString("error")) && content.getAsString("error") != null){
HTTPResponse newRes = new HTTPResponse(401);
newRes.setStatusMessage(httpResponse.getStatusMessage());
newRes.setContent(httpResponse.getContent());
newRes.setWWWAuthenticate(httpResponse.getWWWAuthenticate());
userInfoResponse = UserInfoErrorResponse.parse(newRes);
} else {
userInfoResponse = UserInfoResponse.parse(httpResponse);
}
} catch (ParseException pe){
throw pe;
}

return userInfoResponse;
}

This was not as simple as the token endpoint or the authorize endpoint, but it got the job done.

After completing my implementation I had the realization that this code could be reused if I made it a little more generic and could contribute it back to the common codebase for the product I was working on.

Now the next person that comes into my microservice that needs to make use of calling an OIDC vendor can easily do so, but creating a configuration object from known data, and then call three methods and everything else is taken care of for them.

Using Nimbus certainly made my job much easier and I hope it can help someone else along the way.

Comments

Popular Posts