OpenID Connect authentication

Requesting an authorisation code

Java example how to make an OpenID authentication request to obtain an OAuth 2.0 authorisation code from an OpenID provider.

Since the AuthenticationRequest naturally extends the OAuth 2.0 AuthorizationRequest you can check the OAuth 2.0 authorisation request examples for explanation of the basic concepts.

Client-side code to create an OpenID authentication request:

import java.net.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;

// The client ID provisioned by the OpenID provider when
// the client was registered
ClientID clientID = new ClientID("123");

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

// Generate random state string to securely pair the callback to this request
State state = new State();

// Generate nonce for the ID token
Nonce nonce = new Nonce();

// Compose the OpenID authentication request (for the code flow)
AuthenticationRequest request = new AuthenticationRequest.Builder(
    new ResponseType("code"),
    new Scope("openid"),
    clientID,
    callback)
    .endpointURI(new URI("https://c2id.com/login"))
    .state(state)
    .nonce(nonce)
    .build();

// The URI to send the user-user browser to the OpenID provider
// E.g.
// https://c2id.com/login?
// client_id=123
// &response_type=code
// &scope=openid
// &redirect_uri=https%3A%2F%2Fclient.com%2Fcallback
// &state=6SK5S15Lwdp3Pem_55m-ayudGwno0eglKq6ZEWaykG8
// &nonce=d_Y4LmbzpNHTkzTKJv6v59-OmqB_F2kNr8CbL-R2xWI
System.out.println(request.toURI());

Parsing the OpenID authentication response at the callback URI:

import java.net.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;

// When the call back URI is invoked the response parameters
// will be encoded in the query string, parse them
// https://client.com/callback?
// state=6SK5S15Lwdp3Pem_55m-ayudGwno0eglKq6ZEWaykG8
// &code=eemeuWi9reingee0
AuthenticationResponse response = AuthenticationResponseParser.parse(
    new URI("https://client.com/callback?" +
            "state=6SK5S15Lwdp3Pem_55m-ayudGwno0eglKq6ZEWaykG8" +
            "&code=eemeuWi9reingee0"));

// Check the state
if (! response.getState().equals(state)) {
    System.err.println("Unexpected authentication response");
    return;
}

if (response instanceof AuthenticationErrorResponse) {
    // The OpenID provider returned an error
    System.err.println(response.toErrorResponse().getErrorObject());
    return;
}

// Retrieve the authorisation code, to use it later at the token endpoint
AuthorizationCode code = response.toSuccessResponse().getAuthorizationCode();

Decoding an OpenID authentication request on the OpenID provider side:

import java.net.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;

// Get the query string
String queryString = "https://c2id.com/login?response_type=code&client_id=123...";

// Decode the query string
AuthenticationRequest request = AuthenticationRequest.parse(queryString);

// Extract the parameters

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

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

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

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

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

// Other parameters....


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

// Create the response and output for redirection back to the client
new AuthenticationSuccessResponse(redirectURI, code, null, null, state, null, null)
    .toURI();

Requesting a specific OpenID claim

This is an example request for one or more specific OpenID claims (user attributes), using the optional claims parameter of the OpenID authentication request.

The general method for requesting OpenID claims is by specifying a scope value, such as profile, which is a shortcut that expands to the following list of standard claims: name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, localeupdated_at.

In many cases, however, the specific method for requesting claims with the dedicated claims parameter is better suited:

  • To comply with privacy and data minimisation regulations, such as GDPR, so that only the minimum claims required for the application's function are requested, and not the whole set to which a scope value such as profile typically expands to.

  • To request a custom (non-standard) claim that doesn't map from a scope value.

  • To receive selected claims included in the ID token, instead of obtaining them at the default location -- the UserInfo endpoint.

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

Example request for a https://claims.c2id.com/group claim:

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

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

This will output a request URL like

https://c2id.com/login?
 response_type=code
 &client_id=000123
 &redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
 &scope=openid
 &state=AIGZ1yzIKYi1S8LELVEkdwtI3Qa2Sk5jA2Q0tYkUlkA
 &claims=%7B%22userinfo%22%3A%7B%22group%22%3Anull%7D%7D

To have the https://claims.c2id.com/group claim returned with the ID token:

ClaimsRequest claims = new ClaimsRequest();
claims.addIDTokenClaim("https://claims.c2id.com/group");

To specify multiple claims, for return in the ID token and / or the UserInfo endpoint:

ClaimsRequest claims = new ClaimsRequest();
claims.addUserInfoClaim("https://claims.c2id.com/group");
claims.addIDTokenClaim("email");
claims.addIDTokenClaim("email_address");

Signed OpenID authentication requests

An OpenID authentication request can include parameters which are signed (JWS) and even additionally encrypted (JWE), ensuring their integrity, authenticity and confidentiality.

This is done by putting the parameters in a JWT called request object, which can be included as query parameter by value, or referenced by URL.

The JWT can include all OpenID authentication request parameters, or only selected ones.

Use in FAPI

FAPI is a hardened OAuth 2.0 profile for securing high value resources, such as Open Banking APIs.

FAPI makes use of signed OpenID authentication requests to ensure the integrity protection and authenticity of the parameters, and an ID token returned in the authorisation response to act as its detached signature.

import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;

// Create the client RSA public / private key pair and store it securely.
// The same key can be used to create the self-signed client certificate
// for mTLS client authentication at the token endpoint and receiving
// certificate bound tokens
RSAKey rsaJWK = new RSAKeyGenerator(2048)
    .keyUse(KeyUse.SIGNATURE)
    .algorithm(JWSAlgorithm.PS256)
    .keyIDFromThumbprint(true)
    .generate();

Making a FAPI-compliant OpenID authentication request where the parameters are secured in a signed JWT:

// The required OpenID provider parameters
URI authorizationEndpoint = new URI("https://c2id.com/login");

// Required client parameters from the OpenID relying party registration
ClientID clientID = new ClientID("123");
URI redirectURI = new URI("https://example.com/cb");
JWSAlgorithm requestObjectJWSAlg = JWSAlgorithm.PS256;

// Construct the OpenID authentication request whose parameters
// are going to be signed

// Generate unique state to pair the callback to this request
State state = new State();

// Generate unique nonce for the ID token
Nonce nonce = new Nonce();

AuthenticationRequest securedRequest = new AuthenticationRequest.Builder(
    new ResponseType(ResponseType.Value.CODE, OIDCResponseTypeValue.ID_TOKEN),
    new Scope("openid", "https://scopes.c2id.com/account"),
    clientID,
    redirectURI)
    .state(state)
    .nonce(nonce)
    .acrValues(Collections.singletonList(new ACR("https://trust-frameworks.c2id.com/high")))
    .build();

// Convert params to JWT and sign with the client RSA key
JWTClaimsSet jwtClaimsSet = securedRequest.toJWTClaimsSet();

// {
//   "response_type" : "code id_token",
//   "scope"         : "openid https://scopes.c2id.com/account",
//   "client_id"     : "123",
//   "redirect_uri"  : "https://example.com/cb",
//   "state"         : "hFhArLZR2qV9ghH3pWuVE8tp6SLx2gUB4UC59nAcMKc",
//   "nonce"         : "QP5F-z0xnqhbKE_ZTbJfTZUyCntaxyM4r_NdP3EpOP0",
//   "acr_values"    : "https://trust-frameworks.c2id.com/high"
// }

SignedJWT requestJWT = new SignedJWT(
    new JWSHeader.Builder(requestObjectJWSAlg)
        .keyID(rsaJWK.getKeyID())
    .build(),
    jwtClaimsSet);

JWSSigner jwsSigner = new RSASSASigner(rsaJWK);
// May need an alternative JCA provider for JWS PS256 if the
// algorithm isn't supported by the default JCA provider
jwsSigner.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance());
requestJWT.sign(jwsSigner);

// Construct the final OpenID authentication request which
// includes the signed parameters in a JWT; some top-level
// query parameters are repeated to ensure the request still
// parses an OpenID authentication request
AuthenticationRequest finalRequest = new AuthenticationRequest.Builder(
    securedRequest.getResponseType(),
    new Scope("openid"),
    securedRequest.getClientID(),
    securedRequest.getRedirectionURI())
    .requestObject(requestJWT)
    .endpointURI(authorizationEndpoint)
    .build();

// Output as URI to send the end-user to the OpenID provider
System.out.println(finalRequest.toURI());

// https://c2id.com/login?
//  response_type=code+id_token
//  &scope=openid
//  &client_id=123
//  &redirect_uri=https%3A%2F%2Fexample.com%2Fcb
//  &request=eyJraWQiOiIwNWxJQzZ0dEU4UHV6bmVMSG05LUJyWmc5UW1oUTdpQ1A1ZlVNZ2lPbm
//  VJIiwiYWxnIjoiUFMyNTYifQ.eyJzY29wZSI6Im9wZW5pZCBodHRwczpcL1wvc2NvcGVzLmMyaW
//  QuY29tXC9hY2NvdW50IiwiYWNyX3ZhbHVlcyI6Imh0dHBzOlwvXC90cnVzdC1mcmFtZXdvcmtzL
//  mMyaWQuY29tXC9oaWdoIiwicmVzcG9uc2VfdHlwZSI6ImNvZGUgaWRfdG9rZW4iLCJyZWRpcmVj
//  dF91cmkiOiJodHRwczpcL1wvZXhhbXBsZS5jb21cL2NiIiwic3RhdGUiOiJLSEgycXVzOHhWbmh
//  yeldxakNsUWhLaHlCVElDd3AxTzdKNjdEenNkWTQwIiwibm9uY2UiOiIwRkJrVFNyMlViM2djUU
//  VSUk16MHFqTFh3bWR0NUdzaG1uX2VXbEY4YmxnIiwiY2xpZW50X2lkIjoiMTIzIn0.UW7MZglwY
//  xen23FKDrQ9jGiiS5omCZQmlF4LCIPES860Cgj_YO9_Wx1Hp8lfLkf3Gxd0Vr8KvZAwKVgpsrRu
//  XFz5ht_GzemkLJCzMrHa6txcYuhqcPE_vT0ZmIKipRFwV06AC44BX4r424-zXf71oDSoCXQqFSA
//  cRzem2vfjRqbp9RdPWlhOlqATSoCcXg8wz0m2XgIyYgOeeM6jbnCFg6J2s0Lhi113-dTpCGel5e
//  9T-LATpf2xGVbv-kV7SUkfTcBVzWdyGqGwVdyP-fMOi4QRHdjJWLhG5ExgISeatanDtjiV6fl_Z
//  9tkyBCg6EKhb6tj5RwZ_StsodfZFs_Vvg

Use with software statements

Signed OpenID authentication requests can be 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 with 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, 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 point 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.