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 6.23 of the OpenID Connect SDK.

OpenID authentication request for verified claims with purpose parameter

import java.net.*;
import java.util.*;
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
ClaimsRequest claimsRequest = new ClaimsRequest();
claimsRequest.addVerifiedUserInfoClaim(new ClaimsRequest.Entry("given_name"));
claimsRequest.addVerifiedUserInfoClaim(new ClaimsRequest.Entry("family_name"));
claimsRequest.addVerifiedUserInfoClaim(new ClaimsRequest.Entry("address"));

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);

boolean withLangTags = false;
System.out.println(authRequest.getClaims().getVerifiedUserInfoClaimNames(withLangTags));
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;

ClaimsRequest claimsRequest = new ClaimsRequest();

// Request for regular email claim, default settings
claimsRequest.addUserInfoClaim(new ClaimsRequest.Entry("email"));

// Request for verified name claim, marked as essential, with purpose message
claimsRequest.addVerifiedUserInfoClaim(new ClaimsRequest.Entry("name")
    .withClaimRequirement(ClaimRequirement.ESSENTIAL)
    .withPurpose("Name required for contract"));

// Request for verified address claim, marked as essential, with purpose message
claimsRequest.addVerifiedUserInfoClaim(new ClaimsRequest.Entry("address")
    .withClaimRequirement(ClaimRequirement.ESSENTIAL)
    .withPurpose("Address required for contract"));

// Adding a verification element
JSONObject userInfoVerification = new JSONObject();
userInfoVerification.put("trust_framework", IdentityTrustFramework.EIDAS_IAL_HIGH.getValue());
claimsRequest.setUserInfoClaimsVerificationJSONObject(userInfoVerification);

// 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\"" +
    "        }" +
    "      }" +
    "    }" +
    "  }" +
    "}";

ClaimsRequest claimsRequest = ClaimsRequest.parse(json);

// Get UserInfo verification element if any
System.out.println(claimsRequest.getUserInfoClaimsVerificationJSONObject());

// Get requested verified claims at UserInfo endpoint if any
for (ClaimsRequest.Entry en: claimsRequest.getVerifiedUserInfoClaims()) {
    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
for (ClaimsRequest.Entry en: claimsRequest.getUserInfoClaims()) {
    System.out.println("claim name: " + en.getClaimName());
    System.out.println("requirement: " + en.getClaimRequirement());
    System.out.println("optional language tag: " + en.getLangTag());
}

// Repeat for claims delivered with ID token if any
System.out.println(claimsRequest.getIDTokenClaimsVerificationJSONObject());

for (ClaimsRequest.Entry en: claimsRequest.getVerifiedIDTokenClaims()) {
    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());
}

for (ClaimsRequest.Entry en: claimsRequest.getIDTokenClaims()) {
    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.getVerifiedClaimsSet();

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]"}