The attestation providers are the issuers of credentials like PID, MDL or other documents the users may posses. Also there is not only one attestation provider but multiple which could be a university or a government authority.
Here we will show how you could fetch a credential via a Credential Offer flow from an attestation provider. As trigger we assume the user did scan a QR code, whose content contains an URL that is triggering the application via the defined URL schemes from the setup process.
The SDK will provide the Attestations object for
handling interactions with credentials and attestation providers.
AttestationsAlso the SDK provides some identifier for known attestation types e.g. PID that can be used to filter for credentials.
EUDI_PID_IDENTIFIER
EUDI_WIA_IDENTIFIER
EUDI_WUA_IDENTIFIERNew ones will be added here in time e.g. for MDL.
The openid-credential-offer scheme will be received by
the application via a deeplink. So the deeplink handling has to be
implemented by the application. If the
openid-credential-offer scheme is received the application
has to forward this URL to the wallet SDK, which will parse all needed
information from the string and check its validity and processes it
further to contact the attestation provider (and its authentication
server as needed).
In this flow the CredentialOfferInfo object will be
returned containing the information which credential(s) will be
downloaded. This can be accepted by the user and then the authentication
step is started (jump to external browser) to authenticate the user.
After the authentication a redirect (eu.europa.ec.euidi)
for re-opening the application will happen, and the authorized download
will fetch the credential and store it on the device.
Notice: the PAR (pre authorized request) flow does not need the authorization step via the external browser.
// The provided Flow will send different events that have to be continued to get to the next step of the offer flow (AsyncSequence).
// - CredentialOfferInfo object returned to see what will be downloaded
// - Authenticate at the attestation provider (browser) to allow access to the credential
// - After authentication redirect start the actual download of the credential
let deepLink = "openid-credential-offer://" // from the received deep link
// The function will provide `CredentialOfferEvent` objects
WalletSdk.shared.attestations.retrieveFromCredentialOffer(credentialOfferIntentData: deepLinks)
// returned deep link from authentication to procceed with the download
WalletSdk.shared.attestations.continueWithAuthCodeUrl(authCodeUrl: _url.relativeString)public func fetchCredentialOffer(with _urlData: String) async throws(CredentialOfferError) -> ([CredentialOfferInfo], URL?) {
var array: [CredentialOfferInfo] = []
for await value in WalletSdk.shared.attestations.retrieveFromCredentialOffer(credentialOfferIntentData: _urlData) {
if let error = CredentialOfferError.from(event: value) {
throw error // End of flow (AsyncSequence)
}
switch onEnum(of: value) {
case .infoEvent(let _event):
// received CredentialOfferInfo containing information about the credentials too download
array = _event.credentialOfferInfo
_event.result.complete(value: KotlinUnit()) // Trigger to process for auth-url for download
continue
case .openUrlForAuthEvent(let _event):
// Auth-URL for downloading, used with `continueWithAuthCodeUrl(authCodeUrl)` SDK functionality
// to start the authentication process (will include a user activity to start the process of downloading)
let authURL = _event.authorizationUrl
return (array, URL(string: authURL)) // end of flow (AsyncSequence)
default:
// Error, end of flow (AsyncSequence)
throw CredentialOfferError.unknown
}
}
return (array, nil)
}
public enum CredentialOfferError: Error {
case parsing
case infoParsing
case issuance
case notImplemented
case authenticationURLMissing
case unknown
/// Create from SDK Credential Offer Event an error if one occurred otherwise nil if successful
/// - Parameter _error: the event to check if an error occurred
/// - Returns: nil if successful, otherwise an error is returned
public static func from(event _event: wallet_sdk.CredentialOfferEvent) -> CredentialOfferError? {
switch onEnum(of: _event) {
case .infoEvent:
// successful
return nil
case .openUrlForAuthEvent:
// successful
return nil
case .parsingError:
return CredentialOfferError.parsing
case .infoParsingError:
return CredentialOfferError.infoParsing
case .notImplementedError:
return CredentialOfferError.notImplemented
case .issuanceError:
return CredentialOfferError.issuance
}
}
}public func fetchCredential(withAuthCodeURL _url: URL) async throws(CredentialDownloadError) {
for await value in WalletSdk.shared.attestations.continueWithAuthCodeUrl(authCodeUrl: _url.relativeString) {
if let error = CredentialDownloadError.from(event: value) {
throw error
}
switch onEnum(of: value) {
case .success:
// Success, sdk will trigger an update for the new/updated credential
return
default:
// Error
throw CredentialDownloadError.unknown
}
}
}
public enum CredentialDownloadError: Error {
case unknown
case parsing
case saving
case invalidAppData
/// Create from SDK Credential Download Event an error if one occurred otherwise nil if successful
/// - Parameter _error: the download event to check if an error occurred
/// - Returns: nil if successful, otherwise an error is returned
public static func from(event _event: wallet_sdk.CredentialDownloadEvent) -> CredentialDownloadError? {
switch onEnum(of: _event) {
case .success:
// successful download
return nil
case .parsingError:
return CredentialDownloadError.parsing
case .savingError:
return CredentialDownloadError.saving
case .invalidAppData:
return CredentialDownloadError.invalidAppData
}
}
}The wallet SDK will inform the app about updates in the database for the attestations (credentials) on the device.
As the application wants to display credentials - the user has
downloaded on their device - it can use the provided observable property
attestations from the wallet SDK Attestations
object. The attestations are an Array of
Attestation objects presenting a credential to the wallet.
So the wallet app always interacts with the Attestation
object when fetching the list of credentials or displaying a detail page
for a credential.
Task { @concurrent in
for await array in WalletSdk.shared.attestations.attestations {
await MainActor.run {
// Update ui with the updated credentials
array ...
}
}
}The above AsyncSequence is going to be updated, whenever a change in the wallets database is happening to the credentials.
To load a specific credential e.g. to display all credential
information (claims) to the user, the SDK provides an
getAttestation(attestationId) function. This function will
also return an Attestation object representing the
credential.
let value: Credential? = WalletSdk.shared.attestations.getAttestation(attestationId: "ID of the credential to load")This is an async function which will provide an AsyncSequence. So you can also use it to observe this specific credential on the detail page so the detail page can be updated, if this credential is modified in the database.
Task { @concurrent [weak _queue] in
for await value in WalletSdk.shared.attestations.getAttestation(attestationId: "ID of the credential to observe") {
guard let value = value else {
// no credential found for specified ID, stop the sequence
break
}
// credential update for specified ID
// e.g. update some ui
continue
}
}For presenting the credentials details to a user, the wallet SDK
provides functions on the Attestation object, which will
return claims and also automatically translated label
values and differentiable claim types to display claim values
differently.
The Attestation object provided by the SDK will present
a credential outside of the SDK, providing all the following
functions.
let attestation: Attestation = some attestation
let claimDictionary: [String: Claim] = attestation.claimsThere are different types of claims which the wallet
SDK will return. Claim itself is a
protocol.
The Claim protocol can have different implementations
e.g.:
MapClaim: nested claims to split upStringClaim: directly presentableListClaim: array of claims to go throughImageClaim: data object Data presenting
the imageTo get the translated labels for a credential’s claim values, the SDK provides following functions:
let attestation: Attestation = some attestation
let claim = attestation.claims["some key"]
// 'de' provided by device local from app
let translatedLabel = mapClaim.getDisplayLabel(languageKey: "de",
fallbackLanguageKey: "en")In the provided example a fallback language will be provided, if the primary one is not existing the fallback language will be used. So the application does not need to build translations for the labels on its own.
Also the wallet SDK does provide the claim’s data in different types, so the app e.g. can differentiate a pure StringClaim value from a ListClaim value.
let attestation: Attestation = some attestation
let claimDictionary: [String: Claim] = attestation.claims
// ...
// StringClaim
let valueToDisplay = stringClaim.value as? String
// ListClaim
let valueToDisplay = listClaim.value.joined(separator: ", ")
// MapClaim
let value:s [String, Claim] = mapClaim.value // to run through nested claims
// ImageClaim
let valueToDisplay: Data = imageClaim.value // image Data convert with UIImage(data:) to get the imageThe PID provider is a special attestation provider since it does not have to support every wallet. Normally a PID provider and Wallet Provider will be working together for the wallet that is supported to access credentials on the PID provider.
To receive a PID the above flow can be used and no additional steps or data is needed.