Identity assurance / eKYC
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 9.21 of this SDK and the eKYC / Identity Assurance specification from September 2021 (draft 12).
How to request verified claims with OpenID Connect
This is a simple example OpenID authentication request for an ID token and
release of given_name
, family_name
and address
as verified user claims at
the UserInfo endpoint. No trust framework is specified, which will pick the
default one handled by the identity provider.
import java.net.*;
import java.util.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.pkce.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
// The requested verified claims
OIDCClaimsRequest claimsRequest = new OIDCClaimsRequest()
.withUserInfoVerifiedClaimsRequest(
new VerifiedClaimsSetRequest()
.add("given_name")
.add("family_name")
.add("address")
);
// The above translates to the "claims" JSON object:
// {
// "verification" : {
// "trust_framework" : null
// },
// "claims" : {
// "given_name" : null,
// "family_name" : null,
// "address" : null
// },
// }
// Extra PKCE security
CodeVerifier pkceVerifier = new CodeVerifier();
// Purpose to display
String purpose = "Account holder identification";
// 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("kum8aiy0Ilai6ohD"))
.codeChallenge(pkceVerifier, CodeChallengeMethod.S256)
.claims(claimsRequest)
.purpose(purpose)
.endpointURI(URI.create("https://c2id.com/login"))
.build();
URI request = authRequest.toURI();
// Resulting URI with OpenID authentication request
// https://c2id.com/login?
// response_type=code
// &scope=openid
// &client_id=123
// &redirect_uri=https%3A%2F%2Fexample.com%2Fcb
// &state=kum8aiy0Ilai6ohD
// &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
// &purpose=Account+holder+identification
The application can specify a preferred trust framework like this:
// Attest the user identity according to eIDAS
VerificationSpec verification = new MinimalVerificationSpec(
IdentityTrustFramework.EIDAS
);
// Include the verification spec in the claims request
OIDCClaimsRequest claimsRequest = new OIDCClaimsRequest()
.withUserInfoVerifiedClaimsRequest(
new VerifiedClaimsSetRequest()
.withVerification(verification)
.add("given_name")
.add("family_name")
.add("address")
);
The resulting claims
JSON object:
{
"verification" : {
"trust_framework" : {
"value" : "eidas"
}
},
"claims" : {
"given_name" : null,
"family_name" : null,
"address" : null
},
}
Custom verification
elements can be created by implementing the
VerificationSpec
interface or by extending the
MinimalVerificationSpec
class.
When using the interface implement the toJSONObject
method:
import net.minidev.json.JSONObject;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
public class MyVerificationSpec implements VerificationSpec {
@Override
public JSONObject toJSONObject() {
JSONObject o = new JSONObject();
// compose my custom verification element here
return o;
}
}
Example extension of the MinimalVerificationSpec class which adds a method for specifying the request of evidence attachments:
import net.minidev.json.JSONObject;
import com.nimbusds.openid.connect.sdk.assurance.*;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
public class VerificationWithOptionalAttachments extends MinimalVerificationSpec {
public VerificationWithOptionalAttachments(IdentityTrustFramework framework) {
super(framework);
}
public void includeAttachments(boolean includeAttachments) {
// Modify the protected jsonObject member;
// the JSON object output is then handled by the parent class
if (includeAttachments) {
jsonObject.put("attachments", null);
} else {
jsonObject.remove("attachments");
}
}
}
On the server side the OpenID authentication request can be parsed to retrieve
the elements of the request
JSON object parameter in a type-safe manner. The
request
parameter is represented by the
OIDCClaimsRequest
class. For each supported verified claim the identity provider can use the
methods of
ClaimsSetRequest.Entry
to retrieve the optional claim requirements (voluntary vs essential), preferred
value(s), language tag(s) and purpose to display. Custom parameters in the spec
for a given claim are also supported.
Example parsing of an OpenID authentication request for verified UserInfo:
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.assurance.claims.*;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
// IdP server parsing the OpenID request
AuthenticationRequest authRequest = AuthenticationRequest.parse(request);
// The spec allows for multiple verified claims sets
for (VerifiedClaimsSetRequest vcsr: claimsRequest.getUserInfoVerifiedClaimsRequests()) {
// Print the raw verification object
System.out.println(vcsr.getVerification().toJSONObject());
// Print the name of each requested claim and its options
for (OIDCClaimsSetRequest.Entry en: vcsr.getEntries()) {
System.out.println(en.getClaimName());
System.out.println(en.getClaimRequirement());
System.out.println(en.getValue());
System.out.println(en.getPurpose());
}
}
// Print the preferred UI locales (if any)
System.out.println(authRequest.getUILocales());
// Print the purpose to display (if any)
System.out.println(authRequest.getPurpose());
Claims parameter with options
How to create a claims
request parameter which specifies regular as well as
verified claims for return in the ID token, with use of the essential
and
purpose
options:
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.assurance.*;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
// Attest the user identity according to eIDAS
VerificationSpec verification = new MinimalVerificationSpec(
IdentityTrustFramework.EIDAS
);
OIDCClaimsRequest claimsRequest = new OIDCClaimsRequest()
.withIDTokenClaimsRequest(
new ClaimsSetRequest()
// Request for regular "email" claim, default settings
.add("email")
)
.withIDTokenVerifiedClaimsRequest(
new VerifiedClaimsSetRequest()
// Requested verification details
.withVerification(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")));
// The claims parameter as JSON object
System.out.println(claimsRequest.toJSONString());
The resulting claims
JSON object:
{
"id_token" : {
"email" : null,
"verified_claims" : {
"verification" : {
"trust_framework" : {
"value" : "eidas"
}
},
"claims" : {
"name" : {
"essential" : true,
"purpose" : "Name required for contract"
},
"address" : {
"essential" : true,
"purpose" : "Address required for contract"
}
}
}
}
}
How can an OpenID provider parse and process the claims parameter
An identity provider for verified claims will typically examine the claims
parameter of the OpenID authentication request with the help of logic like
this:
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.assurance.request.*;
private static void print(OIDCClaimsRequest claimsRequest) {
// Get the requested verified claims set for UserInfo delivery
int num = 1;
for (VerifiedClaimsSetRequest claimsSetRequest: claimsRequest.getUserInfoVerifiedClaimsRequests()) {
System.out.println("UserInfo set #" + num++ + ":");
print(claimsSetRequest);
}
// Get the requested verified claims set for ID token delivery
num = 1;
for (VerifiedClaimsSetRequest claimsSetRequest: claimsRequest.getIDTokenVerifiedClaimsRequests()) {
System.out.println("UserInfo set #" + num++ + ":");
print(claimsSetRequest);
}
}
private static void print(VerifiedClaimsSetRequest verifiedClaimsSetRequest) {
VerificationSpec verification = verifiedClaimsSetRequest.getVerification();
System.out.println("\tVerification: " + verification.toJSONObject());
System.out.println("\tRequested claims: ");
for (ClaimsSetRequest.Entry en: verifiedClaimsSetRequest.getEntries()) {
System.out.println("\t\tname: " + en.getClaimName());
System.out.println("\t\t\trequirement: " + en.getClaimRequirement());
if (en.getRawValue() != null) {
// Use claim specific typed value getter
System.out.println("\t\t\tvalue: " + en.getValueAsString());
}
if (en.getLangTag() != null) {
System.out.println("\t\t\tlanguage tag: " + en.getLangTag());
}
if (en.getPurpose() != null) {
System.out.println("\t\t\tpurpose message: " + en.getPurpose());
}
}
}
How to use the above logic with a received OpenID authentication request:
import com.nimbusds.openid.connect.sdk.*;
// Parse the OpenID authentication request
AuthenticationRequest request = AuthenticationRequest.parse(...);
// Check if the claims parameter is set
OIDCClaims claims = request.getOIDCClaims();
if (claims != null) {
// Inspect the claims parameter
print(claims);
}
Applying this logic to the above example produces the following output:
UserInfo set #1:
Verification: {"trust_framework":{"value":"eidas"}}
Requested claims:
name: name
requirement: ESSENTIAL
purpose message: Name required for contract
name: address
requirement: ESSENTIAL
purpose message: Address required for contract
How to compose a UserInfo response with verified claims
A UserInfo response that includes verified claims is constructed by creating a
VerifiedClaimsSet
container to hold the verification data and the claims. Every aspect of this is
made type-safe to prevent developer mistakes and ensure the resulting JSON
object will comply with the JSON schema for the verified_claims
.
Example UserInfo with verified given_name
, family_name
and email
claims,
using the eidas
trust framework:
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 data
Date now = new Date();
DateWithTimeZoneOffset timestamp = new DateWithTimeZoneOffset(
now,
TimeZone.getDefault());
IdentityVerification verification = new IdentityVerification(
IdentityTrustFramework.EIDAS,
IdentityAssuranceLevel.SUBSTANTIAL,
null,
timestamp,
new VerificationProcess(UUID.randomUUID().toString()),
new ElectronicSignatureEvidence(
new SignatureType("qes_eidas"),
new Issuer("https://qes-provider.org"),
new SerialNumber("cc58176d-6cd4-4d9d-bad9-50981ad3ee1f"),
timestamp,
null));
// The verified claims
PersonClaims claims = new PersonClaims();
claims.setGivenName("Alice");
claims.setFamilyName("Adams");
claims.setEmailAddress("alice@wonderland.com");
VerifiedClaimsSet verifiedClaims = new VerifiedClaimsSet(
verification,
claims);
UserInfo userInfo = new UserInfo(new Subject("alice"));
userInfo.setVerifiedClaims(verifiedClaims);
// Print the UserInfo JSON object
System.out.println(userInfo.toJSONObject());
Example output for the UserInfo JSON object:
{
"sub" : "alice",
"verified_claims" : {
"verification" : {
"trust_framework" : "eidas",
"assurance_level" : "substantial",
"time" : "2022-01-19T13:45:13+02:00",
"verification_process": "1629ba78-2935-4603-a4fe-173f8608d282",
"evidence" : [ {
"type" : "electronic_signature",
"signature_type" : "qes_eidas",
"issuer" : "https://qes-provider.org",
"serial_number" : "cc58176d-6cd4-4d9d-bad9-50981ad3ee1f",
"created_at" : "2022-01-19T13:45:13+02:00"
} ]
},
"claims" : {
"given_name" : "Alice",
"family_name" : "Adams",
"email" : "alice@wonderland.com"
}
}
}
Note that following the principles of privacy and data minimisation an OpenID provider must not return claims and verification data that isn’t explicitly requested by the relying party.
In regard to the verification data, one implementation strategy for OpenID providers is to construct a complete IdentityVerification, serialise it to a JSON object, and then use the verification element from the request as template to filter those details that are explicitly requested by the relying party and can therefore remain to be merged into the final UserInfo response (or ID token claims).
How to parse a UserInfo response with verified claims
Parsing and processing of the response can proceed in a similar type-safe manner:
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.*;
import com.nimbusds.openid.connect.sdk.assurance.*;
import com.nimbusds.openid.connect.sdk.assurance.claims.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.*;
import com.nimbusds.openid.connect.sdk.claims.*;
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());
if (userInfo.getVerifiedClaims() == null) {
System.out.println("No verified claims found");
return;
}
for (VerifiedClaimsSet verifiedClaims: userInfo.getVerifiedClaims()) {
IdentityVerification verification = verifiedClaims.getVerification();
System.out.println("Trust framework: " + verification.getTrustFramework());
System.out.println("Assurance level: " + verification.getAssuranceLevel());
System.out.println("Time: " + verification.getVerificationTime());
System.out.println("Verification process: " + verification.getVerificationProcess());
if (verification.getEvidence() != null) {
for (IdentityEvidence ev : verification.getEvidence()) {
System.out.println("Evidence type: " + ev.getEvidenceType());
// Proceed further if necessary...
}
}
System.out.println("Verified claims: ");
PersonClaims claims = verifiedClaims.getClaimsSet();
System.out.println("Given name: " + claims.getGivenName());
System.out.println("Family name: " + claims.getFamilyName());
System.out.println("Email: " + claims.getEmailAddress());
}
Example output:
Trust framework: eidas
Assurance level: substantial
Time: 2022-01-19T14:25:40+02:00
Verification process: 94bccc14-cf2b-416e-af62-88120ecab702
Evidence type: electronic_signature
Verified claims:
Given name: Alice
Family name: Adams
Email: alice@wonderland.com
How to deal with attachments
An IdentityEvidence used in the verification can come with one or more attachments (if requested by the relying party and supplied by the OpenID provider).
Those are represented by the abstract Attachment class which can be:
-
EmbeddedAttachment
– for attachments of type
embedded
and delivered inline; will typically only work for UserInfo responses and less so for ID tokens. -
ExternalAttachment
– for attachments of type
external
to be retrieved at a secured URL.
The content of an embedded attachment can be retrieved immediately, that of an external attachment with help of the retrieveContent method (handles the optional token and the required digest validation internally):
// Some evidence in the verification data
IdentityEvidence evidence = ...;
// Set appropriate HTTP timeouts (in milliseconds)
// for the external attachments
int httpConnectTimeout = 4_000;
int httpReadTimeout = 5_000;
if (evidence.getAttachments() != null) {
for (Attachment attachment: evidence.getAttachments()) {
// Get the attachment content
Content content = null;
if (AttachmentType.EMBEDDED.equals(attachment.getType()) {
// Embedded attachment
content = attachment.toEmbeddedAttachment().getContent();
} else {
// External attachment
try {
content = attachment
.toExternalAttachment()
.retrieveContent(httpConnentTimeout, httpReadTimeout);
} catch (IOException | NoSuchAlgorithmException | DigestMismatchException e) {
System.err.println(e.getMessage());
continue;
}
}
// Save / process the attachment
// The MIME / Content-Type
System.out.println(content.getType());
// The BASE64-encoded content
System.out.println(content.getBase64().toString());
// The optional description
System.out.println(content.getDescription());
}
}
When processing attachments relying parties should have a list of accepted content types and ignore or reject ones that are not understood. The ContentType class includes constants for all popular image formats, such as PNG, JPEG and PDF.
ISO 3166-1 and 3166-3 country codes
When dealing with nationalities, birthplaces and addresses the eKYC / Identity Assurance claims and verification data make use of standard ISO country codes. The codes ensure countries are represented unambiguously in passports and database records.
In the com.nimbusds.openid.connect.sdk.assurance.claims package the SDK provides concrete classes for dealing with the ISO country codes in a robust and type-safe manner:
-
ISO3166_1Alpha2CountryCode – for 2-letter country codes, e.g. “AD” for Andorra.
-
ISO3166_1Alpha3CountryCode – for 3-letter country codes, e.g. “AND” for Andorra.
-
ISO3166_3CountryCode – for special 4-letter codes for former countries and territories, required in cases such a country of birth that no longer officially exists, e.g. “CSHH” for Czechoslovakia.
-
CountryCode – abstract class for dealing with country codes.
Example parsing of an ISO-3166 alpha-2 (two letter) country code:
import com.nimbusds.openid.connect.sdk.assurance.claims.*;
// Parse a country code and check its length
CountryCode code = CountryCode.parse("AD");
assertEquals(2, code.length());
// Cast to alpha-2 country code
ISO3166_1Alpha2CountryCode alpha2Code = countryCode.toISO3166_1Alpha2CountryCode();
The ISO3166_1Alpha2CountryCode and ISO3166_1Alpha3CountryCode classes include constants for all official country codes. Those can also be queried to obtain the country name as spelled out in English in the ISO country code registry.
ISO3166_1Alpha3CountryCode alpha3Code = ISO3166_1Alpha3CountryCode.AND;
assertEquals("Andorra", alpha3Code.getCountryName());
The two classes also provide useful methods for mapping between alpha-2 and alpha-3 codes.
// Get the matching two letter code for AND
ISO3166_1Alpha3CountryCode alpha3Code = ISO3166_1Alpha3CountryCode.AND;
ISO3166_1Alpha2CountryCode alpha2Code = alpha3Code.toAlpha2CountryCode();
assertEquals(ISO3166_1Alpha2CountryCode.AD, alpha2Code);
How to query OpenID provider support for verified claims
The support for verified claims can be queried at the well-known endpoint where OpenID providers publish their metadata. The eKYC / Identity Assurance spec defines several metadata parameters to advertise the available trust frameworks, the names of the suppoted verified claims and other details.
Example request to obtain OpenID provider metadata and display its eKYC / Identity Assurance support (based on this example):
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.*;
import com.nimbusds.openid.connect.sdk.assurance.evidences.attachment.*;
import com.nimbusds.openid.connect.sdk.op.*;
// The OpenID provider issuer URL
Issuer issuer = new Issuer("https://demo.c2id.com");
// The OpenID provider issuer URL
Issuer issuer = new Issuer("https://demo.c2id.com");
// Will resolve the OpenID provider metadata automatically
OIDCProviderMetadata opMetadata = OIDCProviderMetadata.resolve(issuer);
// Show the available eKYC / Identity Assurance support
if (! opMetadata.supportsVerifiedClaims()) {
// eKYC / IdA not supported
return;
}
System.out.println("Trust frameworks: " + opMetadata.getIdentityTrustFrameworks());
System.out.println("Verified claims: " + opMetadata.getVerifiedClaims());
System.out.println("Evidence types: " + opMetadata.getIdentityEvidenceTypes());
if (opMetadata.getIdentityEvidenceTypes().contains(IdentityEvidenceType.DOCUMENT)) {
System.out.println("Document types: " + opMetadata.getDocumentTypes());
System.out.println("Document methods: " + opMetadata.getDocumentMethods());
System.out.println("Document validation methods: " + opMetadata.getDocumentValidationMethods());
System.out.println("Document verification methods: " + opMetadata.getDocumentVerificationMethods());
}
if (opMetadata.getIdentityEvidenceTypes().contains(IdentityEvidenceType.ELECTRONIC_RECORD)) {
System.out.println("Electronic record types: " + opMetadata.getElectronicRecordTypes());
}
if (opMetadata.getAttachmentTypes() != null) {
System.out.println("Attachment types: " + opMetadata.getAttachmentTypes());
if (opMetadata.getAttachmentTypes().contains(AttachmentType.EXTERNAL)) {
System.out.println("Hash algorithms: " + opMetadata.getAttachmentDigestAlgs());
}
}