OpenID Connect authentication

Requesting an authorisation code

Code examples how to make an OpenID authentication request to obtain a code (in the OAuth 2.0 authorisation code flow) or token (in the implicit flow) from the OpenID provider.

To compose an OpenID authentication request on the client side (in the code flow):

// The client identifier provisioned by the server
ClientID clientID = new Client("123");

// The client callback URL
URL callback = new URL("https://client.com/callback");

// Generate random state string for pairing the response to the request
State state = new State();

// Generate nonce
Nonce nonce = new Nonce();

// Compose the request (in code flow)
AuthenticationRequest req = new AuthenticationRequest(
    new URL("https://c2id.com/login"),
    new ResponseType("code"),
    Scope.parse("openid email profile address"),
    clientID,
    callback,
    state,
    nonce);

HTTPResponse httpResponse = req.toHTTPRequest().send();

AuthenticationResponse response = AuthenticationResponseParser.parse(httpResponse);

if (response instanceof AuthenticationErrorResponse) {
    // process error
}

AuthenticationSuccessResponse succesResponse =
    (AuthenticationSuccessResponse)response;

// Retrieve the authorisation code
AuthorizationCode code = successResponse.getAuthorizationCode();

// Don't forget to check the state
assert successResponse.getState().equals(state);

Decoding the OpenID authentication request on the server side:

// Get the query string
String query = "https://server.example.com/op/authorize?response_type=code&client_id=123...";

// Decode the query string
AuthenticationRequest req = AuthenticationRequest(null, query);

// Extract the parameters

// Required to look up the client in the provider's database
ClientID clientID = req.getClientID();

// The client redirection URL, must be registered in the provider's database
URL redirectURI = req.getRedirectionURI();

// The response type (implies code flow)
ResponseType rt = req.getResponseType();

// The state, must be echoed back with the response
State state = req.getState();

// The requested scope
Scope scope = req.getScope();

// Other parameters....

// Process the request and generate a code
AuthorizationCode code = new AuthorizationCode();

// Create response
AuthenticationSuccessResponse(redirectURI, code, null, null, state);

// Output the response depending on your web server framework
// ...

Requesting a specific OpenID claim

This is an example request for a specific OpenID claim, using the optional claims parameter.

You may choose to take this approach for requesting one or more claims in the following situations:

  • When you have a custom claim that is not mapped from a scope value. For example you have an LDAP directory feeding a custom "group" claim that is not tied to a particular claim.

  • When you want to request a specific claim only, e.g. just name without asking for the whole kitchen sink of the profile scope value (from which name maps).

  • When you want to receive selected claims included in the ID token, instead of obtaining them via the UserInfo endpoint.

Note that some OpenID providers do not support this parameter (but the Connect2id server does).

Example:

// Make specific request for the group claim, to be returned at the UserInfo endpoint
ClaimsRequest claims = new ClaimsRequest();
claims.addUserInfoClaim("group");

AuthenticationRequest request = new AuthenticationRequest.Builder(
    new ResponseType("code"),
    new Scope("openid"),
    new ClientID("000123"),
    URI.create("https://demo.c2id.com/oidc-client/callback"))
    .state(new State())
    .claims(claims)
    .endpointURI(URI.create("https://demo.c2id.com/login-page-js"))
    .build();

URI requestURI = request.toURI();

This will output a request URL like

https://demo.c2id.com/c2id-login-page-js?
    response_type=code
    &client_id=000123
    &redirect_uri=https%3A%2F%2Fdemo.c2id.com%2Foidc-client%2Fcb
    &scope=openid
    &state=AIGZ1yzIKYi1S8LELVEkdwtI3Qa2Sk5jA2Q0tYkUlkA
    &claims=%7B%22userinfo%22%3A%7B%22group%22%3Anull%7D%7D

To have the group claim returned with the ID token:

ClaimsRequest claims = new ClaimsRequest();
claims.addIDTokenClaim("group");

// ...

Signed OpenID authentication requests

Publishers of apps can use sealed OpenID authentication requests to ensure their parameters cannot be modified or tampered with.

This approach can be especially useful for mobile apps that register dynamically with an OpenID provider:

  1. The software publisher creates a software statement that locks down the client registration parameters, such as the grant types to use, mandating PKCE, the scopes that may be requested, and client metadata such as app name, icon, and links (possibly with l10n). The software statement (a JSON Web Token) is signed by the software publisher and put into the distributed app package. The JWT can be additionally encrypted for confidentiality.

  2. The software publisher also creates a signed JWT that locks down the parameters of OpenID authentication requests to be made from a client instance to the OpenID provider. These locked down parameters may include response_type, client_id, scope, redirect_uri and any other parameter that is otherwise supported. This JWT can be included in the app package, or uploaded to a public https URL. Additional encryption is possible too.

  3. Upon installation the app instance registers itself with the OpenID provider of choice. The OpenID provider is previously configured to only accept client registrations from approved software publishers. Unless the signature validation of the software statement succeeds, the client is not allowed to register.

  4. Subsequent OpenID authentication requests include the specified JWT, either directly by value, or by referencing its URL. The latter approach enables updates of the OpenID request parameters without necessitating an update to the app itself.

How to create an OpenID connect request that utilises signed parameters?

Suppose the app publisher wants to lock down the request_type, scope and code_challenge_method parameters like this:

  • request_type=code
  • scope=openid email
  • code_challenge_method=S256

Create a JSON Web Token with the said parameters, then sign it:

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;

JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
    .claim("response_type", "code")
    .claim("scope", "openid email")
    .claim("code_challenge_method", "S256")
    .build();

SignedJWT jwt = new SignedJWT(
    new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(keyID).build(),
    jwtClaims);

jwt.sign(new RSASSASigner(rsaPrivateKey));

String jwtString = jwt.serialize();

This is an example JWT containing the above claims:

eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZSI6Im9wZW5pZCBlbWFpbCIsInJlc3BvbnNl
X3R5cGUiOiJjb2RlIiwiY29kZV9jaGFsbGVuZ2VfbWV0aG9kIjoiUzI1NiJ9.jbkSsZycG8j4CQJwhd
0-JZU1LFAytQ8IjNfOQzDghwqNnpe_lYTz_OU5lPG9UzagFuJWsmS4uQRbtFV6ROLlRr2TYbkOrMYEz
pim8JvqTjiFQBC3ds2yDdTYwxJknPJhVKw8F9dv_lWREI8AXGzhkbpDckhJHEERvi-esbRwryQ

If the signed parameters are to be included in the OpenID authentication request by value:

import java.net.URI;

import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.pkce.*;
import com.nimbusds.openid.connect.sdk*;

// Compute PKCE
CodeVerifier pkceVerifier = new CodeVerifier();

URI authRequest = new AuthenticationRequest.Builder(
    new ResponseType("code"),
    new Scope("openid"),
    new ClientID("123"),
    URI.create("myapp://openid-connect-callback"))
    .state(new State())
    .codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
    .requestObject(jwt)
    .endpointURI(URI.create("https://openid.c2id.com"))
    .build()
    .toURI();

The resulting URL to redirect to the OpenID provider with the authentication request:

https://openid.c2id.com?response_type=code
    &client_id=123
    &redirect_uri=myapp%3A%2F%2Fopenid-connect-callback
    &scope=openid
    &state=-67ztq9L0k6dQiyqEjU-jfPCd40lN-ZsaDQAwLrY1Ro
    &code_challenge=Oea7ws0BUXkKXADTumdSYj41gQi-VBFYSq_JwqgvX8E
    &code_challenge_method=S256
    &request=eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZSI6Im9wZW5pZCBlbWFpbCI
    sInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwiY29kZV9jaGFsbGVuZ2VfbWV0aG9kIjoiUzI1NiJ9.J
    XEKGXOjzzXjSkW-Ilcl8oy9ixRNf4NbEK5jgIsWhtbKjDalVbE8Ix38gNCcH6zuq5x6K3fHkFl6
    pvf0ViIdjX8SH8fZt5odZ1cmLmQp-x5DI3Pb5oNADvp-wGbiQ9NtV24yIoa8rxt7xN9mcsINxvm
    REpfLjx8PbC8R1qpxvWc

To pass the signed parameters by URL reference, simply upload the JWT to a web server. OpenID authentication requests must then reference this URL.

If you intend to update the signed parameters at some in future, append the SHA-256 hash of the content to the URL fragment. Because OpenID providers may cache the JWT URL, this is the suggested mechanism for signalling that the JWT has changed and must be fetched again.


// Compute the JWT hash and append it as fragment to the URL
Base64URL fragment = Base64URL.encode(MessageDigest.getInstance("SHA-256").digest(jwtString.getBytes(Charset.forName("UTF-8"))));
URI requestURI = URI.create("https://myapp.io/request.jwt+" + fragment);

URI authRequest = new AuthenticationRequest.Builder(
    new ResponseType("code"),
    new Scope("openid"),
    new ClientID("123"),
    URI.create("myapp://openid-connect-callback"))
    .state(new State())
    .codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
    .requestURI(requestURI)
    .endpointURI(URI.create("https://openid.c2id.com"))
    .build()
    .toURI();

The resulting URL to redirect to the OpenID provider with the authentication request will look almost identical, save for the using request_uri to point to the signed parameters:

https://openid.c2id.com?response_type=code
    &client_id=123
    &redirect_uri=myapp%3A%2F%2Fopenid-connect-callback
    &scope=openid
    &state=-67ztq9L0k6dQiyqEjU-jfPCd40lN-ZsaDQAwLrY1Ro
    &code_challenge=Oea7ws0BUXkKXADTumdSYj41gQi-VBFYSq_JwqgvX8E
    &code_challenge_method=S256
    &request_uri=https%3A%2F%2Fmyapp.io%2Frequest.jwt%2BWhDZ3rM2e76DcqsZOXVwAj2C_l4QzxWhoFPvYHZQkpI

Important to know:

  1. The request parameters present in the JWT override any top-level parameters of the OpenID authentication request.

  2. If a request_type or client_id is present in the JWT, it must match the top-level parameters, else the OpenID provider will return an error.

  3. When you take out the parameters included in the JWT, the OpenID authentication request must still qualify as a valid request. Otherwise the OpenID provider will return an invalid_request error. The SDK enforces this automatically for you.

The Connect2id server supports signed OpenID authentication requests from version 6 on.