@affinidi/wallet-core-sdk
v7.19.1
Published
SDK core monorepo for affinity DID solution
Downloads
716
Readme
Affinity Core SDK - Affinity network DID solution
WARNING Action required from you
Update your services to use Affinidi SDK v6.0.4 or above.
Note please pay attention to the changelog while upgrading the version of SDK as some methods may be changed or deprecated. If you are using Affinidi SDK below v6, your application doesn’t support Affinidi Vault and hence we cannot migrate you out of the Bloom Vault.
With Affinidi SDK v6.0.4 onwards, we have also introduced automatic trigger of migration to Affinidi Vault and that is why we ask you to upgrade to that version or above.
Otherwise your credentials will never be migrated. The migration will not anyhow impact SDK performance negatively.
Furthermore, if you have more than 100 credentials in Bloom Vault the performance should be increased after migration. Bloom Vault is no longer supported with Affinidi SDK v7.
Table of contents
- Workflow schemas:
- How to install
- Set Up Integration Tests
- Initialize
- Interface
- Dependencies on Affinidi infra
How to install
npm i --save @affinidi/wallet-core-sdk
Setup Integration Tests
Test credentials should be added to the top level .env
file. These contain usernames and passwords of pre-populated accounts on the staging environment. Reach out to a team member for instructions on how to set up this file, or to obtain a copy.
You can also run integration tests against dev
:
TEST_AGAINST=dev npm run test:integration
Initialize
Create API-KEY
You should register your entity at Affinity for appropriate environment
staging,
production or
dev,
to obtain the apiKey
and apiKeyHash
values, one of which should be passed
via options
as a required parameter.
If you want to specify issuer's URL, pass it in the options.
You can also specify the stack environment to be used in env
variable.
env
- (optional) is enum which can be dev
| staging
| prod
(staging
is used by default).
const options = {
env: 'staging',
apiKey: 'YOUR API KEY'
}
OR
const options = {
env: 'staging',
accessApiKey: 'YOUR API KEY HASH VALUE'
}
encryptedSeed
- randomly auto generated by Generate seed more detail example material that used as input for a private key generation. Generated as a part of register / signUp process and stored as encrypted value in protected vault.
seed itself never exposed to the public and available in encrypted form as a property of wallet
.
accountNumber
- optional parameter to derive custom account keys
/did
from the root seed
(could be used to create several ephemeral DIDs to support not traceability option).
const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, encryptedSeed, password, accountNumber)
Issuer / Holder / Verifier interface examples
Set up SDK options
options
is a required parameter for wallet initialization.
You can specify optional field didMethod
and skipAnchoringForElemMethod
in options
const options = {
env: 'staging',
apiKey: 'YOUR API KEY',
didMethod: '...', // 'elem' (default), 'jolo', 'elem-anchored', 'did:web', 'did:key' and 'did:polygon'
skipAnchoringForElemMethod: true
}
If skipAnchoringForElemMethod
is set to true
and didMethod
is elem
, DID anchoring will be skipped.
By default, didMethod
is elem
and skipAnchoringForElemMethod
is false
, so anchoring will not be skipped unless explicitly specified.
Create a new wallet
'elem-anchored' did method returns did:elem, and it is anchored with sidetree in ropsten testnet
note: elem-anchored doesn't support external keys due to Sidetree protocol limitations
IMPORTANT NOTICE: Please be informed that this Ropsten is a Testnet and an environment deployed for testing purposes. Please do not include sensitive or valuable information during your trials on the Testnet. Ropsten does not guaranty the consistency, stability or uninterrupted access to the functionalities provided herein and before as well as during the testing activities the user is informed and understands that any information and connections introduced, uploaded or generated during your interactions with the Testnet can and will be removed at any time at the sole criteria of Ropsten.
const options = {
env: 'staging',
apiKey: '....',
webDomain: 'identity.actor:alice' // required parameter in case `web` is passed as didMethod
didMethod: '....' // 'elem' (default), 'jolo' or 'elem-anchored', 'web', 'key' and 'polygon'
}
const wallet = await AffinidiWallet.createWallet(options, password)
const { didDocument } = wallet // for web did method didDocument is returned
// In case of web did method for others to be able to resolve this did - need to host it
// at your domain which was provided as webDomain
// `:` using as path (replacing to the `/` at url),
// e.g. webDomain: 'identity.actor:alice' should hosted at https://identity.actor/alice/did.json
Sign DID document
const signedDidDocument = await wallet.signDidDocument(didDocument)
Interface
const { AffinidiWallet } = require('@affinidi/wallet-{PLATFORM}-sdk') // where platform is one of 'browser', 'expo', 'node', 'react-native'
Create DID
const wallet = await AffinidiWallet.createWallet(options, password)
const { did, encryptedSeed } = wallet
Intiate SDK
const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, password, encryptedSeed)
Get DID
const wallet = AffinidiWallet.openWalletByEncryptedSeed(options, password, encryptedSeed)
const did = wallet.did
did
- user's DID
Passwordless sign in or sign up if user does not exist + DID creation
NOTE: This passwordless method was designed for a specific usecase, and its
simple user interface achieved by making extra calls to AWS Cognito.
If this method is chosen for authentication you may see failed requests
to Cognito in the browser's console - that is expected behaviour,
exceptions are caught and handled properly.
const token = await AffinidiWallet.initiateSignInPasswordless(options, username, messageParameters)
username
- email or phoneNumber, of existing Cognito user or if it
does not exist, a new one will be created.
IMPORTANT: Username is case sensitive, so 2 separate accounts will be created
on sign up for `[email protected]` and `[email protected]`.
In case you want to have a case-agnostic behaviour, please resolve this
on the application layer by normalizing the input before passing it to the SDK
(e.g. email.toLowerCase()).
messageParameters
- (optional) used to specify message, htmlMessage, subject, see signup method.
Confirm sign in
const { isNew, wallet } = await AffinidiWallet.completeSignInPasswordless(token, confirmationCode, options)
token
- from previous step.
confirmationCode
- 6 digits code, generated and sent by AWS Cognito/SES.
Returns isNew
flag, identifying whether new account was created, and
initialized instance of SDK - wallet
.
Sign up
We STRONGLY recommend using a password at least 8 characters, but it's allowed to be 6 min (in this case - salt as username hash and special character will be added on signup, and the same rule will be applied on login cases)
const wallet = await AffinidiWallet.signUpWithUsername(options, username, password, messageParameters)
or
const token = await AffinidiWallet.initiateSignUpByEmail(options, email, password, messageParameters)
or
const token = await AffinidiWallet.initiateSignUpByPhone(options, phone, password, messageParameters)
Password recovery is not possible unless user have registered with email or phone number or have added email or phone number later.
IMPORTANT: Username is case sensitive, so 2 separate accounts will be created
on sign up for `[email protected]` and `[email protected]`.
In case you want to have a case-agnostic behaviour, please resolve this
on the application layer by normalizing the input before passing it to the SDK
(e.g. email.toLowerCase()).
password
- optional.
Requirements: min length 8, require number, upper and lowercase letter.
NOTE: password is optional if username is email or phone number only.
If not provided, user will be able to login with passwordless flow only
(initiateLogInPasswordless
+ completeLogInPasswordless
, with OTP submit).
When username is arbitrary username, password must be provided.
messageParameters
- optional
const htmlMessage = `
<table align="center" border="1" cellpadding="0" cellspacing="0" width="600">
<tr>
<td bgcolor="#70bbd9">
here is your {{CODE}}.
</td>
</tr>
</table>
`
const messageParameters = {
message: 'Welcome to Affinity, your OTP: {{CODE}}'
subject?: 'Your verification Code'
htmlMessage?
}
{{CODE}} - will be replaced at the message by OTP
If htmlMessage not provided, meesage parameter will be used
To finish registration:
NOTE: email/SMS with verification code (OTP) will be sent to the provided email/phoneNumber, unless username is an arbitrary username.
const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode)
token
- token from the previous step (value returned from the initiateSignUp
).
confirmationCode
- OTP sent by AWS Cognito/SES. Parameter is required if
email/phoneNumber was given as a username, and is ignored in case when
username is an arbitrary username.
To re-send sign up confirmation code (in case when username is email/phoneNumber):
await AffinidiWallet.resendSignUpConfirmationCode(options, token, messageParameters)
token
- token returned by initiateSignUp
.
messageParameters
- (optional) used to specify message, htmlMessage, subject, see signup method.
Sign up with email/phoneNumber (example)
const email = '[email protected]'
const password = 'Password123'
const options = { env: 'dev' }
const token = await AffinidiWallet.initiateSignUpByEmail(options, email, password)
// OTP is sent out by Cognito
const confirmationCode = '123456' // OTP from email/SMS
const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode)
Now user can login
Sign up with arbitrary username (example)
const username = 'great_user'
const password = 'Password123'
const options = { env: 'dev' }
const wallet = await AffinidiWallet.signUpWithUsername(options, username, password)
Now user can login with username and pasword
Sign up to Affinity Wallet with already created DID/keys. (Create User at Affinity Wallet and store there user keys)
User already have created keys in advance, e.g.
const { did, encryptedSeed } = await AffinidiWallet.createWallet(options, password)
Sign up with already created keys:
const keyParams = { encryptedSeed, password }
const email = '[email protected]'
const userPassword = 'Password123'
const options = { env: 'dev' }
const messageParameters = { message: 'Welcome to Affinity, here is your OTP: {{CODE}}' } // (optional)
const token = await AffinidiWallet.initiateSignUpByEmail(options, email, userPassword, messageParameters)
const confirmationCode = '123456'
const wallet = await AffinidiWallet.completeSignUp(options, token, confirmationCode, keyParams)
Or, alternatively, if an arbitrary username is used
const wallet = await AffinidiWallet.signUpWithUsername(options, username, password, keyParams)
Update Did Document (supported only for jolo method):
init SDK
const wallet = await AffinidiWallet.openWalletByEncryptedSeed(options, encryptedSeed, password)
// OR
const wallet = await AffinidiWallet.logInWithPassword(options, userName, userPassword)
Then
await wallet.updateDidDocument(didDocument)
where didDocument - its valid signed didDocument
Initiate instance of SDK with login and pasword
To initiate instance of wallet using just login and password (when user already signed up at Affinity, and if stored his keys at Affinity Guardian Wallet).
const wallet = await AffinidiWallet.logInWithPassword(options, username, password)
options
- (optional) used to specify environment stack (dev | staging | prod).
Initiate instance of SDK with refreshToken
To initiate instance of wallet using refreshToken.
To get refreshToken use wallet.serializeSession() (for wallets with cognito, when logged in)
Take care about refresh token to save it in right place, its lifetime is 30 days. To invalidate tokens please use wallet.logOut() method. The best way to use only access token and re-login user each hour (lifetime of accessToken)
const wallet = await AffinidiWallet.logInWithRefreshToken(options, refreshToken)
options
- (optional) used to specify environment stack (dev | staging | prod).
Passwordless login
Login to the network by username, registered in AWS Cognito.
const token = await AffinidiWallet.initiateLogInPasswordless(options, login, messageParameters: MessageParameters )
login
- email or phone number, at which confirmation code will be sent.
messageParameters
- (optional) used to specify message, htmlMessage, subject, see signup method.
Complete login challenge and initiate instance of SDK:
const wallet = await AffinidiWallet.completeLoginPasswordless(options, token, confirmationCode)
token
- token from the previous step.
confirmationCode
- 6 digits code, generated and sent by AWS Cognito/SES.
completeLoginPasswordless
could return next errors:
COR-5
Invalid confirmation OTP code was uses. As part of error payload new session token is passed that should be used for continuation of completing a session with old OTP
import retry from "async-retry";
let newToken;
try {
const wallet = await AffinidiWallet.completeLoginPasswordless(options, token, invalidConfirmationCode)
} catch (sdkError) {
if (sdkError.code === 'COR-5') {
newToken = sdkError.context.newToken
}
}
const wallet = await AffinidiWallet.completeLoginPasswordless(options, newToken, confirmationCode)
CAUTION for serverside SDK usage you should always use a token returned with error to continue process
Up to 3 retries are possible. After 3 times new session should be used
COR-13
Invalid confirmation OTP code was used 3 times or more. Use ainitiateLogInPasswordless
call to initiate a new session.COR-17
Confirmation code is expired. Lifetime of confirmation code is around 3 minutes. Use ainitiateLogInPasswordless
call to initiate a new session
Password recovery
NOTE: Password recovery is not possible with arbitrary username.
const token = await AffinidiWallet.initiateForgotPassword(options, login, messageParameters)
username
- email or phone number, at which confirmation code will be sent.
messageParameters
- (optional) used to specify message, htmlMessage, subject, see signup method.
Complete change password challenge:
const wallet = await AffinidiWallet.completeForgotPassword(options, token, confirmationCode)
token
- token returned by initiatePassword
confirmationCode
- 6 digits code, generated and sent by AWS Cognito/SES.
newPassword
- new password for Cognito user.
Change password
User have to be logged in to change password. Otherwise use password recovery.
await wallet.changePassword(oldPassword, newPassword)
oldPassword
- old password.
newPassword
- new password.
Change username
const token = await wallet.initiateChangeEmail(newEmail)
or
const token = await wallet.initiateChangePhone(newPhone)
newEmail
or newPhone
- email address or phone number to be later used to log into cognito
Complete change username challenge:
await wallet.completeChangeEmailOrPhone(options, token, confirmationCode)
token
- token returned on the previous step.
confirmationCode
- 6 digits code, generated and sent by AWS Cognito/SES.
Sign Out
Signs out current user from all devices.
It also invalidates all refresh tokens issued to a user.
The user's current access and Id tokens remain valid until their expiry.
Access and Id tokens expire one hour after they are issued.
await wallet.logOut()
Serialize session
Returns all active cognito tokens as json string
{ "accessToken": "eyJraWQiOiJHiLCJlxKU...", "idToken": 'eyJraWQiOiJQYTVjZFwvKzVyb...", "refreshToken": 'eyJjdHkiOiJKV1QiLCJ...", "expiresIn": 1655544042964 }
await wallet.serializeSession()
Issuer
Initiate credential offer request
const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }
const credentialOfferToken = await issuerWallet.generateCredentialOfferRequestToken(offeredCredentials, jwtOptions)
audienceDid
(string) - audience of genreated token.
expiresAt
(isoString) - expires of genreated token.
nonce
(string) - nonce/jti of genreated token.
callbackUrl
(string)
Initiate DID auth
const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }
const authDidRequestToken = await issuerWallet.generateDidAuthRequest(jwtOptions)
audienceDid
(string) - audience of genreated token.
expiresAt
(isoString) - expires of genreated token.
nonce
(string) - nonce/jti of genreated token.
callbackUrl
(string)
const offeredCredentials = [
{
type: 'IssuerCustomCredential',
renderInfo,
},
]
renderInfo
(optional) where issuer can define how that VC can be represented/shown.
Example:
const renderInfo = {
logo: {
url: 'https://miro.medium.com/fit/c/240/240/1*jbb5WdcAvaY1uVdCjX1XVg.png',
},
background: {
url: 'https://i.imgur.com/0Mrldei.png',
},
text: {
color: '#05050d',
},
}
credentialOfferToken
can be passed to the wallet side, and let Wallet/Holder option to response
to this offer if usser want take offered credentials.
Validate Holder Response on Offer Request
const { isValid, did, nonce, selectedCredentials } = await issuer.verifyCredentialOfferResponseToken(
credentialOfferResponseToken,
credentialOfferRequestToken,
)
credentialOfferRequestToken
(optional) - using when need check response against request (nonce, audience).
Validates response token and verify signature, if verification not passed response { isValid: false }
if response is valid returns also { issuer, nonce, selectedCredentials }
.
did
- it's DID which signed that response.
Sign multiple credentials
import { VCV1Unsigned } from '@affinidi/vc-common'
import { VCSPhonePersonV1, getVCPhonePersonV1Context } from '@affinidi/vc-data'
import { buildVCV1Unsigned, buildVCV1Skeleton } from '@affinidi/vc-common'
const unsignedCredentials: VCV1Unsigned[] = [
buildVCV1Unsigned({
skeleton: buildVCV1Skeleton<VCSPhonePersonV1>({
id: 'urn:urn-5:...',
credentialSubject: {
data: {
'@type': ['Person', 'PersonE', 'PhonePerson'],
telephone: '+1 555 555 5555',
},
},
holder: { id: 'did:...:...' },
type: 'PhoneCredentialPersonV1',
context: getVCPhonePersonV1Context(),
}),
issuanceDate: new Date().toISOString(),
expirationDate: new Date(new Date().getTime() + 10 * 60 * 1000).toISOString(),
}),
]
const credentials = await signCredentials(credentialOfferResponseToken, unsignedCredentials)
credentialOfferResponseToken
- credential offer response JWT.
credentialParams
- array of params for credentials, where expiresAt
is optional.
Generate signed credential
import { VCSPhonePersonV1, getVCPhonePersonV1Context } from '@affinidi/vc-data'
const credentialSubject: VCSPhonePersonV1 = {
data: {
'@type': ['Person', 'PersonE', 'PhonePerson'],
telephone: '+1 555 555 5555',
},
}
const credentialMetadata = {
context: [getVCPhonePersonV1Context()],
name: 'Phone Number',
type: ['PhoneCredentialPersonV1'],
}
const credential = await issuer.signCredential(
credentialSubject,
credentialMetadata,
{ credentialOfferResponseToken, requesterDid },
expiresAt,
)
credentialSubject
- data which should be present in VC according to VC schema, must be a valid VCV1Subject
.
credentialMetadata
- schema of credential (should be defined and Issuer and Verifier use the same,
so verifier will be able to understand what kind of credential was created by Issuer).
Revocation
SDK Support Issuing Revocable Credential based on Revocation List 2020 W3C standard
Revocation Flow
issuance of Revocable credential
const unsignedCredential = buildVCV1Unsigned({
skeleton: buildVCV1Skeleton<VCSPhonePersonV1>({
id: `credId:${credId}`,
credentialSubject: {
data: {
'@type': ['Person', 'PersonE', 'PhonePerson'],
telephone: '+1 555 555 5555',
},
},
holder: { id: holderDid },
type: 'PhoneCredentialPersonV1',
context: getVCPhonePersonV1Context(),
}),
issuanceDate: new Date().toISOString(),
expirationDate: new Date(new Date().getTime() + 10 * 60 * 1000).toISOString(),
})
const revokableUnsignedCredential = await wallet.buildRevocationListStatus(
unsignedCredential,
accessToken,
)
buildRevocationListStatus
will add to unsigned credential special credentialStatus
field with revocationlist2020status data.
Revocation of Revocable credential
await wallet.revokeCredential(credentialId, 'Status changed', accessToken)
credentialId
- id of credential for revoke
revocation reason
- free text reason of revocation
accessToken
Verifier
Initiate Verifiable Presentation request (credential share request)
Verifiable Presentation according to w3c spec structure flow:
const jwtOptions = { audienceDid, expiresAt, nonce, callbackUrl }
const presentationChallenge = await verifier.generatePresentationChallenge(
credentialRequirements,
issuerDid,
jwtOptions,
)
audienceDid
(string) - audience of genreated token.
expiresAt
(isoString) - expires of genreated token.
nonce
(number) - nonce/jti of genreated token.
callbackUrl
(string)
Generates JWT with info of which VC credentialRequirements
to be provided from Wallet/Holder.
const credentialRequirements = [{ type: ['Credential', 'ProofOfNameCredential'] }]
callbackUrl
- (optional) Holder/Wallet will be able send response on this request to this URL.
issuerDid
- (optional) its contrain, that define required isser of VC.
credentialShareRequestToken
can be send to Wallet/Holder to anwser on this with response with requested VC inside.
Verifiable Presentation as JWT method:
const credentialShareRequestToken = await verifier.generateCredentialShareRequestToken(
credentialRequirements,
issuerDid,
options,
)
see parameters description at Verifiable Presentation according to w3c spec
section
Validate Verifiable Presentation (Holder Response on Share Request)
Verifiable Presentation according to w3c spec structure flow:
const { isValid, did, challenge, suppliedPresentation } = await verifier.verifyPresentation(vp)
Verifiable Presentation as JWT method:
const { isValid, did, nonce, suppliedCredentials } = await verifier.verifyCredentialShareResponseToken(
credentialShareResponseToken,
credentialShareRequestToken,
shouldOwn,
)
credentialShareResponseToken
- (optional) using when need check response against request (when request have constrains).
shouldOwn
- (optional) Verify that subject is holder of VC. Default true as per W3C spec.
Its validate response token and verify signature on provided VC inside, if verification not passed response { isValid: false }
.
If response is valid it returns also { did, nonce, suppliedCredentials }
.
Validate Holder Response on Did auth Request
const { isValid, did, nonce } = await verifier.verifyDidAuthResponse(authDidResponseToken, authDidRequestToken)
Its validate response token, if verification not passed response { isValid: false }
if response is valid returns also { did, nonce }
Wallet
Initialize region for storing credentials
You can specify AWS region where user credentials will be stored using optional
storageRegion
parameter (region should be a 3 character string correlating to
an Alpha-3 country code).
const options = {
storageRegion: 'SGP'
}
Create Verifiable Presentation (Response on credential share request)
Verifiable Presentation according to w3c spec structure flow:
const vp = await wallet.createPresentationFromChallenge(
presentationChallenge,
credentials,
domain,
)
credentials
- credentials which Holder providing for Verifier.
callbackURL
- (optional)
domain
- (could be empty string)
Verifiable Presentation as JWT method:
const responseToken = await wallet.createCredentialShareResponseToken(
credentialShareRequestToken,
suppliedCredentials,
expiresAt,
)
credentialShareRequestToken
(jwt) - previously generated request token.
suppliedCredentials
- credentials which Holder providing for Verifier.
expiresAt
(isoString) - (optional) expires of created token.
Create Response on credential offer request
const responseToken = await wallet.createCredentialOfferResponseToken(credentialOfferRequestToken)
Agree to recieve proposed credentials by the Issuer.
Create Response on DID auth request
const authDidResponseToken = await wallet.createDidAuthResponse(authDidRequestToken)
Claim Credential from credential offer request
Exchange credentialOfferResponseToken
to a credentials from a callback url specified in request for offer.
const credentials = await wallet.claimCredentials(credentialOfferRequestToken)
Requirements to a callback endpoint
- accept
credentialOfferResponseToken
in a post body
{
"credentialOfferResponseToken": "xxxxx.xxxxx.xxx"
}
- return an array of credentials in a payload
{
"vcs": []
}
Expected errors
COR-26
&COR-19
-credentialOfferRequestToken
validation errorsCOR-27
- failed to invoke callback urlCOR-28
- unsuccessful callback invocation.COR-29
- invalid response format. No credentials
Encrypted messages
Create encrypted message
const encryptedMessage = await wallet.createEncryptedMessage(toDid, object)
toDid
- DID, string value of document to be resolved.
object
- value to be encrypted by public key.
Read encrypted message
const message = await wallet.readEncryptedMessage(encryptedMessage)
encryptedMessage
- message to be decrypted.
Credentials vault
const credentials = [ signedCredential ]
const storageRegion = 'SGP'
await wallet.saveCredentials(credentials, storageRegion)
credentials
- array of credentials to store in the vault.
storageRegion
- (optional) AWS region where user's credentials will be stored.
Region should be a 3 character string correlating to an Alpha-3 country code.
Get all credentials matching the shareRequestToken
const credentials = await wallet.getCredentials(shareRequestToken)
shareRequestToken
- optional parameter (if passed - returns VC,
which match the request, if not - then returns all VCs).
Get all credentials
const credentials = await wallet.getCredentials(null)
Get a single credential
const credential = await wallet.getCredentialById(credentialId)
Delete credential by ID
await wallet.deleteCredential(credentialId)
Affinidi Infra dependencies
This SDK using next Affinidi services:
- affinidi registry (to anchor when applicable, resolve and update did/didDocument)
- affinidi verifier (to build credential request)
- affinidi issuer (to build credential offer and verify credential offer response)
- affinidi wallet backend (to store endrypted seed and encrypted VC optioanlly as backup)
- affinidi user management (using only when backup option for encrypted seed used)