This technical article explores the concept of integrating with Connect ID API directly. There are two related resources:
Why is direct integration via Connect ID API not a recommended approach?
How to integrate with Connect ID when using technology other than .NET/Java/PHP?
Client that will choose a direct API integration, will find code snippets (in TypeScript) for the most common scenarios, i.e.:
- getting a potential duplicate information (without personal details) from the Connect ID Service
- getting personal details of a potential duplicate via Service Bus
Prerequisites
Client attempting to integrate directly with the Connect ID API need:
- understanding of business concepts described here: Integration with Connect ID: step-by-step instruction
- credentials (clientID + secretKey) for a given environment
Intro
Connect ID Service exposes a REST API layer. All methods called by our SDKs use this API under the hood. As indicated in the linked articles, a Client implementing a direct API access would need to solve a number of challenges, e.g.:
- implement integration with Azure Active Directory (authentication & authorization)
- implement encryption/decryption of messages in Service Bus
- implement person registration (single operation in SDK but multiple operations without it)
- etc.
This approach is therefore not recommended.
There may exist scenarios where the benefits of a direct integration could outweigh the costs, e.g.: Client has a number of records (of existing football players) in their DB and they want to assign FIFA_IDs to them. They want to perform an automated assignment of FIFA_IDs for records with a score = 1.0 (100%), but they don't need to extract personal details via the Service Bus. In such a case, it may be quicker to integrate directly with an API instead of implementing the SDK.
Proof of concept 1 - getting duplicate information from the Connect ID Service without personal details
The code snippet below searches for potential duplicates of 'Daniel Robertsen' and returns a FIFA_ID as long as the data matches exactly (i.e. the score = 1.0).
import axios from 'axios'; import { format } from 'date-fns'; import msal from '@azure/msal-node'; const ENV = 'prod'; const CLIENT_ID = ''; // credentials issued by Connect ID const SECRET_KEY = ''; const TENANT = 'fifaconnect.onmicrosoft.com'; const HASHING_SCOPE = `https://${TENANT}/fci-hashing-${ENV}/.default`; const ID_DIRECTORY_SCOPE = `https://${TENANT}/fci-iddirectory-${ENV}/.default`; const getToken = (scope: string) => msalInstance.acquireTokenByClientCredential({ scopes: [scope] }); const getBaseHeaders = (authResult: msal.AuthenticationResult) => ({ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authResult.accessToken}` } }); /* In order to authenticate with FIFA Connect ID, an access token from Azure Active Directory must be acquired. Microsoft provides MSAL libraries in different technologies that allow easy fetching and caching of the tokens. You can opt-out of this and use plain REST API calls, but bear in mind that tokens should be reused between calls. */ const msalInstance = new msal.ConfidentialClientApplication({ auth: { clientId: CLIENT_ID, clientSecret: SECRET_KEY, authority: `https://login.microsoftonline.com/${TENANT}` } }); await (async () => { // hash person names that will be used in the duplicate search const hashResponse = await axios.post(`https://hashing.id.ma.services/api/v6/hash/compute`, undefined, { ...getBaseHeaders(await getToken(HASHING_SCOPE)), params: { firstName: 'Daniel', lastName: 'Robertsen' } }); // get a list of duplicates using person's date of birth, gender and name hashes const duplicatesResponse = await axios.post( `https://directory.id.ma.services//api/v6/person/getduplicates`, { person: { dateOfBirth: format(new Date(1991, 11, 31), 'yyyy-MM-dd'), gender: 'male' }, nameHashs: hashResponse.data }, getBaseHeaders(await getToken(ID_DIRECTORY_SCOPE)) ); const exactMatch = duplicatesResponse.data.duplicates.find((d: { proximityScore: number }) => d.proximityScore === 1); if (exactMatch) { // at this point person's FIFA ID can be saved const personFifaId = exactMatch.personFifaId; } })();
Proof of concept 2 - getting duplicate details via Service Bus
The code snippet below is an extension of the previous example. For all potential duplicates returned, with a score different than 1.0, it requests person details via the Service Bus. The code is much more complicated as the communication over Service Bus is encrypted and requires the public/private key infrastructure.
import axios from 'axios'; import msal from '@azure/msal-node'; import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { v4 as uuid } from 'uuid'; import { format } from 'date-fns'; const connectIdClient = axios.create({ headers: { 'Content-Type': 'application/json' } }); const ENV = 'prod'; const CLIENT_ID = ''; // Credentials issued by Connect ID const SECRET_KEY = ''; const TENANT = 'fifaconnect.onmicrosoft.com'; const PRIVATE_KEY_PATH = './path/to/your/private/key'; const PRIVATE_KEY_PASSPHASE = 'pass_phrase_of_your_private_key'; const HASHING_URL = `https://hashing.id.ma.services/api/v6`; const HASHING_SCOPE = `https://${TENANT}/fci-hashing-${ENV}/.default`; const IDDIRECTORY_URL = `https://directory.id.ma.services/api/v6`; const IDDIRECTORY_SCOPE = `https://${TENANT}/fci-iddirectory-${ENV}/.default`; const SERVICEBUS_URL = `https://bus.id.ma.services/api/v3`; const SERVICEBUS_SCOPE = `https://${TENANT}/fci-servicebus-${ENV}/.default`; type PersonData = { dateOfBirth: string; gender: 'male' | 'female' }; type Duplicate = { personFifaId: string; proximityScore: number; primaryDataProviderRegistrationType: { organisationFIFAId: string; systemId?: string } }; // Create an AES decryptor for incoming messages function Aes(seed?: { key: Buffer; iv: Buffer }) { const key = seed?.key ?? crypto.randomBytes(32); const iv = seed?.iv ?? crypto.randomBytes(16); const encryptionType = 'aes-128-cbc'; const encryptionEncoding = 'base64'; const decrypt = (base64String: string): string => { const buffer = Buffer.from(base64String, encryptionEncoding); const decipher = crypto.createDecipheriv(encryptionType, key, iv); const deciphered = `${decipher.update(buffer)}${decipher.final()}`; return deciphered; }; return { decrypt }; } /* In order to authenticate with FIFA Connect ID, an access token from Azure Active Directory must be acquired. Microsoft provides MSAL libraries in different technologies that allow easy fetching and caching of the tokens. You can opt-out of this and use plain REST API calls, but bear in mind that tokens should be reused between calls. */ const msalInstance = new msal.ConfidentialClientApplication({ auth: { clientId: CLIENT_ID, clientSecret: SECRET_KEY, authority: `https://login.microsoftonline.com/${TENANT}` } }); const createAuthHeader = (authResult: msal.AuthenticationResult) => ({ Authorization: `Bearer ${authResult.accessToken}` }); const baseHeaders = (authResult: msal.AuthenticationResult) => ({ headers: createAuthHeader(authResult) }); const getToken = (scope: string) => msalInstance.acquireTokenByClientCredential({ scopes: [scope] }); const privateKey = fs.readFileSync(path.resolve(PRIVATE_KEY_PATH), 'utf-8'); const rsaDecrypt = (data: Buffer) => crypto.privateDecrypt( { key: privateKey.toString(), passphrase: PRIVATE_KEY_PASSPHASE, padding: crypto.constants.RSA_PKCS1_PADDING }, data ); const computeHashes = async (firstName: string, lastName: string) => { const hashingToken = await getToken(HASHING_SCOPE); return await connectIdClient.post(`${HASHING_URL}/hash/compute`, undefined, { ...baseHeaders(hashingToken), params: { firstName, lastName } }); }; const getDuplicates = async (person: PersonData, nameHashes: []) => { const idDirectoryToken = await getToken(IDDIRECTORY_SCOPE); return await connectIdClient.post<{ duplicates: Duplicate[] }>( `${IDDIRECTORY_URL}/person/getduplicates`, { person, nameHashes }, baseHeaders(idDirectoryToken) ); }; const sendRequestForDetails = async (personFifaId: string, recipient: string, messageCorrelationId: string) => { const serviceBusToken = await getToken(SERVICEBUS_SCOPE); return await connectIdClient.post(`${SERVICEBUS_URL}/message/send`, ' ', { headers: { 'X-Action': 'person-details-request', 'X-Properties': JSON.stringify({ uniqueFifaId: personFifaId, correlationId: messageCorrelationId }), ...createAuthHeader(serviceBusToken) }, params: { recipient } }); }; const markMessageAsProcessed = async (messageId: string, lockToken: string) => { const serviceBusToken = await getToken(SERVICEBUS_SCOPE); await connectIdClient.delete(`${SERVICEBUS_URL}/message/delete`, { ...baseHeaders(serviceBusToken), params: { messageId, lockToken } }); }; const peekLockMessage = async (timeout: number) => { const serviceBusToken = await getToken(SERVICEBUS_SCOPE); const response = await connectIdClient.post(`${SERVICEBUS_URL}/message/peeklock`, undefined, { ...baseHeaders(serviceBusToken), params: { timeout } }); if (response.status === 204) { // No messages available return null; } return { data: response.data, properties: JSON.parse(response.headers['x-properties']), brokerProperties: JSON.parse(response.headers['brokerproperties']) }; }; // Using your private RSA key, decrypt the AES key from incoming message. Then use that AES key and IV to decrypt the message content const aesDecrypt = (data: string, encryptionKey: string, encryptionIV: string) => { const decryptedKey = rsaDecrypt(Buffer.from(encryptionKey, 'base64')); const aes = Aes({ key: decryptedKey, iv: Buffer.from(encryptionIV, 'base64') }); return aes.decrypt(data); }; const tryGetPersonDetailsXml = async (duplicate: Duplicate): Promise<string | null> => { const registration = duplicate.primaryDataProviderRegistrationType; const recipient = registration.systemId ? registration.systemId : registration.organisationFIFAId; const messageCorrelationId = uuid(); // Create an ID that will be used to correlate the request with response // Request person's details from a Data Holder await sendRequestForDetails(duplicate.personFifaId, recipient, messageCorrelationId); // Wait for a total of 30 seconds to receive a message matching your request. // Please note that messages are processed in a queue-like structure, so requests should be issued sequentially. const startTime = Date.now(); while (Date.now() - startTime < 30_000) { const peekLockResponse = await peekLockMessage(30); // Lock a message for processing. Lock will automatically expire after 30 seconds. if (peekLockResponse === null) continue; // There are no messages pending const { data, properties, brokerProperties } = peekLockResponse; // If current message is the one we are waiting for if (properties.correlationId === messageCorrelationId) { try { return aesDecrypt(data, properties['EncryptionKey'], properties['EncryptionInitializationVector']); } finally { await markMessageAsProcessed(brokerProperties['MessageId'], brokerProperties['LockToken']); break; } } } // The system asked for details did not respond within the configured timeout, no data received return null; }; await (async () => { // Hash person names that will be used in the duplicate search const hashResponse = await computeHashes('Daniel', 'Robertsen'); // Get a list of duplicates using person's date of birth, gender and name hashes const duplicatesResponse = await getDuplicates({ dateOfBirth: format(new Date(1991, 11, 31), 'yyyy-MM-dd'), gender: 'male' }, hashResponse.data); for (const duplicate of duplicatesResponse.data.duplicates) { if (duplicate.proximityScore === 1.0) { const personFifaId = duplicate.personFifaId; // Exact match found, can safely save person's FIFA ID } else { // Decide what to do based on the received person details const personDetailsXml = await tryGetPersonDetailsXml(duplicate); } } })();