OpenID Connect eKYC / Identity Assurance

eKYC / Identity Assurance is a new OpenID Connect extension for letting providers of verified identities, such as national eID schemes, banks and eIDAS providers, release UserInfo and ID tokens with special metadata describing which claims are strongly verified, and how the actual verification took place.

The examples below are based on version 8.23 of the OpenID Connect SDK and draft 11 of the eKYC / Identity Assurance specification.

OpenID authentication request for verified claims with purpose parameter

import java.net.*;
import java.util.*;
import net.minidev.json.JSONObject;
import com.nimbusds.langtag.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.pkce.*;
import com.nimbusds.openid.connect.sdk.*;

// Construct an OpenID authentication request

// Extra PKCE security
CodeVerifier pkceVerifier = new CodeVerifier();

// The requested verified claims
// {
//   "verification" : {
//     "trust_framework" : null
//   },
//   "claims" : {
//     "given_name"  : null,
//     "family_name" : null,
//     "address"     : null
//   },
// }
JSONObject verification = new JSONObject();
verification.put("trust_framework", null);

OIDCClaimsRequest claimsRequest = new OIDCClaimsRequest()
    .withUserInfoVerifiedClaimsRequest(
        new VerifiedClaimsSetRequest()
            .withVerificationJSONObject(verification)
            .add("given_name")
            .add("family_name")
            .add("address")
    );

// Compose the OpenID authentication request
AuthenticationRequest authRequest = new AuthenticationRequest.Builder(
    new ResponseType(ResponseType.Value.CODE),
    new Scope(OIDCScopeValue.OPENID),
    new ClientID("123"),
    URI.create("https://example.com/cb"))
    .state(new State("7a4b68ab-5315-4e25-a10f-0fbfaa36d6c7"))
    .codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
    .claims(claimsRequest)
    .uiLocales(Collections.singletonList(new LangTag("en")))
    .purpose("Account holder identification")
    .endpointURI(URI.create("https://c2id.com/authz"))
    .build();

URI request = authRequest.toURI();

// Resulting URI with OpenID authentication request
// https://c2id.com/authz?
// response_type=code
// &scope=openid
// &client_id=123
// &redirect_uri=https%3A%2F%2Fexample.com%2Fcb
// &state=7a4b68ab-5315-4e25-a10f-0fbfaa36d6c7
// &code_challenge_method=S256
// &code_challenge=LnJxED7AfXdPbzbRhvijwUDTvweK4gfDYbN9um_1dis
// &claims=%7B%22userinfo%22%3A%7B%22verified_claims%22%3A%7B%22claims%22%3A%7B%22address%22%3Anull%2C%22given_name%22%3Anull%2C%22family_name%22%3Anull%7D%7D%7D%7D
// &ui_locales=en
// &purpose=Account+holder+identification


// IdP server parsing the request
authRequest = AuthenticationRequest.parse(request);

for (VerifiedClaimsSetRequest vcsr: claimsRequest.getUserInfoVerifiedClaimsRequestList()) {
    System.out.println(vcsr.getVerificationJSONObject());
    for (ClaimsSetRequest.Entry en: vcsr.getEntries()) {
        System.out.println(en.getClaimName());
        System.out.println(en.getClaimRequirement());
        System.out.println(en.getValue());
        System.out.println(en.getPurpose());
    }
}
System.out.println(authRequest.getUILocales());
System.out.println(authRequest.getPurpose());

Claims parameter composition

import net.minidev.json.JSONObject;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.assurance.IdentityTrustFramework;

JSONObject verification = new JSONObject();
verification.put("trust_framework", IdentityTrustFramework.EIDAS_IAL_HIGH.getValue());

OIDCClaimsRequest claimsRequest = new OIDCClaimsRequest()
    .withUserInfoClaimsRequest(
        new ClaimsSetRequest()
            // Request for regular "email" claim, default settings
            .add("email")
    )
    .withUserInfoVerifiedClaimsRequest(
        new VerifiedClaimsSetRequest()
            // Requested verification details
            .withVerificationJSONObject(verification)
            // Request verified "name" claim,
            // marked as essential, with purpose message
            .add(new ClaimsSetRequest.Entry("name")
                .withClaimRequirement(ClaimRequirement.ESSENTIAL)
                .withPurpose("Name required for contract"))
            // Request for verified "address" claim,
            // marked as essential, with purpose message
            .add(new ClaimsSetRequest.Entry("address")
                .withClaimRequirement(ClaimRequirement.ESSENTIAL)
                .withPurpose("Address required for contract")));

// If JSON output of the claims parameter is needed
System.out.println(claimsRequest.toJSONString());

// {
//   "userinfo" : {
//     "email" : null,
//     "verified_claims" : {
//       "verification" : {
//         "trust_framework" : "eidas_ial_high"
//       },
//       "claims" : {
//         "name" : {
//           "essential" : true,
//           "purpose"   : "Name required for contract"
//         },
//         "address" : {
//           "essential" : true,
//           "purpose"   : "Address required for contract"
//         }
//       }
//     }
//   }
// }

Claims parameter processing

import com.nimbusds.openid.connect.sdk.*;

String json = "{" +
    "  \"userinfo\" : {" +
    "    \"email\" : null," +
    "    \"verified_claims\" : {" +
    "      \"verification\" : {" +
    "        \"trust_framework\" : \"eidas_ial_high\"" +
    "      }," +
    "      \"claims\" : {" +
    "        \"name\" : {" +
    "          \"essential\" : true," +
    "          \"purpose\"   : \"Name required for contract\"" +
    "        }," +
    "        \"address\" : {" +
    "          \"essential\" : true," +
    "          \"purpose\"   : \"Address required for contract\"" +
    "        }" +
    "      }" +
    "    }" +
    "  }" +
    "}";

OIDCClaimsRequest claimsRequest = OIDCClaimsRequest.parse(json);

// The request for verified claims can be composed of one or
// more elements, take the first here
VerifiedClaimsSetRequest verifiedRequest = claimsRequest.getUserInfoVerifiedClaimsRequestList().get(0);

// Get the UserInfo verification
System.out.println(verifiedRequest.getVerificationJSONObject());

// Get requested verified claims at UserInfo endpoint if any
for (ClaimsSetRequest.Entry en: verifiedRequest.getEntries()) {
    System.out.println("verified claim name: " + en.getClaimName());
    System.out.println("requirement: " + en.getClaimRequirement());
    System.out.println("optional language tag: " + en.getLangTag());
    System.out.println("optional purpose message: " + en.getPurpose());
}

// Get requested plain claims at UserInfo endpoint if any
if (claimsRequest.getUserInfoClaimsRequest() != null) {
    for (ClaimsSetRequest.Entry en : claimsRequest.getUserInfoClaimsRequest().getEntries()) {
        System.out.println("claim name: " + en.getClaimName());
        System.out.println("requirement: " + en.getClaimRequirement());
        System.out.println("optional language tag: " + en.getLangTag());
    }
}

// Repeat for claims requested for delivery with the ID token if any
verifiedRequest = claimsRequest.getIDTokenVerifiedClaimsRequestList().get(0);

System.out.println(verifiedRequest.getVerificationJSONObject());

for (ClaimsSetRequest.Entry en: verifiedRequest.getEntries()) {
    System.out.println("verified claim name: " + en.getClaimName());
    System.out.println("requirement: " + en.getClaimRequirement());
    System.out.println("optional language tag: " + en.getLangTag());
    System.out.println("optional purpose message: " + en.getPurpose());
}

if (claimsRequest.getIDTokenClaimsRequest() != null) {
    for (ClaimsSetRequest.Entry en : claimsRequest.getIDTokenClaimsRequest().getEntries()) {
        System.out.println("claim name: " + en.getClaimName());
        System.out.println("requirement: " + en.getClaimRequirement());
        System.out.println("optional language tag: " + en.getLangTag());
    }
}

Composing a UserInfo response with verified claims

import java.util.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.util.date.*;
import com.nimbusds.openid.connect.sdk.assurance.*;
import com.nimbusds.openid.connect.sdk.assurance.claims.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.*;

// The verification details for some eIDAS process
Date now = new Date();
DateWithTimeZoneOffset timestamp = new DateWithTimeZoneOffset(
    now,
    TimeZone.getDefault());
String verificationProcess = "4ebc8150-1b26-460a-adc1-3e9096ab88f9";
IdentityVerification verification = new IdentityVerification(
    IdentityTrustFramework.EIDAS_IAL_SUBSTANTIAL,
    timestamp,
    verificationProcess,
    new QESEvidence(
        new Issuer("https://qes-provider.org"),
        "cc58176d-6cd4-4d9d-bad9-50981ad3ee1f",
        DateWithTimeZoneOffset.parseISO8601String("2019-12-01T08:00:00Z")));

// The verified claims
PersonClaims claims = new PersonClaims();
claims.setName("Alice Adams");
claims.setEmailAddress("[email protected]");

// Construct the verified claims component
VerifiedClaimsSet verifiedClaims = new VerifiedClaimsSet(
    verification,
    claims);

// Insert it into a UserInfo object
UserInfo userInfo = new UserInfo(new Subject("alice"));
userInfo.setVerifiedClaims(verifiedClaims);

System.out.println(userInfo.toJSONObject());

// Example output:
// {
//   "sub":"alice",
//   "verified_claims":{
//       "claims":{
//           "name":"Alice Adams",
//           "email":"[email protected]"
//           },
//       "verification":{
//           "trust_framework":"eidas_ial_substantial",
//           "time":"2019-12-04T22:57:16+02:00",
//           "verification_process":"4ebc8150-1b26-460a-adc1-3e9096ab88f9",
//           "evidence":[{
//               "type":"qes",
//               "issuer":"https:\/\/qes-provider.org",
//               "serial_number":"cc58176d-6cd4-4d9d-bad9-50981ad3ee1f",
//               "created_at":"2019-12-01T08:00:00+00:00"
//           }]
//       }
//   }
// }

Parsing a UserInfo response with verified claims

import java.net.*;
import java.util.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.http.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.oauth2.sdk.util.date.*;
import com.nimbusds.openid.connect.sdk.assurance.*;
import com.nimbusds.openid.connect.sdk.assurance.claims.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.*;

HTTPResponse httpResponse = new UserInfoRequest(
    URI.create("https://c2id.com/userinfo"),
    new BearerAccessToken("..."))
    .toHTTPRequest()
    .send();

UserInfoResponse response = UserInfoResponse.parse(httpResponse);

if (! response.indicatesSuccess()) {
    System.err.println(response.toErrorResponse().getErrorObject());
    return;
}

UserInfo userInfo = response.toSuccessResponse().getUserInfo();

System.out.println("Subject: " + userInfo.getSubject());

VerifiedClaimsSet verifiedClaims = userInfo.getVerifiedClaims().get(0);

System.out.println("Trust framework: " + verifiedClaims.getVerification().getTrustFramework());
System.out.println("Evidence type: " + verifiedClaims.getVerification().getEvidence().get(0).getEvidenceType());
System.out.println("Verified claims: " + verifiedClaims.getClaimsSet().toJSONObject());

// Example output:
// Subject: alice
// Trust framework: eidas_ial_substantial
// Evidence type: qes
// Verified claims: {"name":"Alice Adams","email":"[email protected]"}