Create an identity#
To create an account on the Concordium blockchain, a user must first acquire an identity. Therefore, as an initial step, a wallet user will always have to create an identity before being able to send account transactions.
An identity is acquired by generating an identity request and sending it to an identity provider. The user will then be taken through the identity verification process that is specific for that chosen identity provider. This happens outside of the wallet application.
Create an identity request#
The first step is to create the actual identity request. To do this, you need the list of identity providers. Refer to Get list of identity providers and their metadata to understand how to retrieve it.
import {
ConcordiumGRPCWebClient,
ConcordiumHdWallet,
createIdentityRequestWithKeys,
IdentityRequestWithKeysInput,
IdObjectRequestV1,
Versioned,
} from '@concordium/web-sdk';
// Select the identity provider that is to be used for creating the identity. Here we
// choose the first one in the list available from the Concordium wallet-proxy.
const identityProvider: IdentityProviderWithMetadata = getIdentityProviders()[0];
// The index of the identity to create. The index is incremented per identity created
// for a specific identity provider. For the first identity created it should be 0,
// for the second it should be 1, and so forth.
const identityIndex = 0;
// Some global cryptographic properties are required as input. They can be retrieved from
// a Concordium node using the gRPC interface. Note that these are not specific for this
// identity and therefore can be re-used.
const client = new ConcordiumGRPCWebClient(nodeAddress, nodePort);
const cryptographicParameters = await client.getCryptographicParameters();
// Derive the secret key material and randomness from the Concordium wallet.
const seedPhrase = 'fence tongue sell large master side flock bronze ice accident what humble bring heart swear record valley party jar caution horn cushion endorse position';
const network = 'Testnet'; // Or Mainnet, if working on mainnet.
const wallet = ConcordiumHdWallet.fromSeedPhrase(seedPhrase, network);
const identityProviderIndex = identityProvider.ipInfo.ipIdentity;
const idCredSec = wallet.getIdCredSec(identityProviderIndex, identityIndex).toString('hex');
const prfKey = wallet.getPrfKey(identityProviderIndex, identityIndex).toString('hex');
const blindingRandomness = wallet.getSignatureBlindingRandomness(identityProviderIndex, identityIndex).toString('hex');
// The anonymity revocation threshold. Here we select the highest possible threshold for
// the chosen identity provider.
const arThreshold = Math.min(Object.keys(identityProvider.arsInfos).length - 1, 255);
// Construct the input for creating the identity request.
const input: IdentityRequestWithKeysInput = {
arsInfos: identityProvider.arsInfos,
arThreshold,
ipInfo: identityProvider.ipInfo,
globalContext: cryptographicParameters,
idCredSec,
prfKey,
blindingRandomness,
};
const identityRequest: Versioned<IdObjectRequestV1> = createIdentityRequestWithKeys(input);
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import com.concordium.sdk.ClientV2
import com.concordium.sdk.Connection
import com.concordium.sdk.TLSConfig
import com.concordium.sdk.crypto.wallet.ConcordiumHdWallet
import com.concordium.sdk.crypto.wallet.Identity
import com.concordium.sdk.crypto.wallet.IdentityRequestInput
import com.concordium.sdk.crypto.wallet.Network
import com.concordium.sdk.requests.BlockQuery
fun createIdentityRequest(): String {
// Select the identity provider that is to be used for creating the identity. Here we
// choose the first one in the list available from the Concordium wallet-proxy.
val identityProvider = getIdentityProviders(walletProxyTestnetBaseUrl)[0]
// The index of the identity to create. The index is incremented per identity created
// for a specific identity provider. For the first identity created it should be 0,
// for the second it should be 1, and so forth.
val identityIndex = 0
val connection = Connection.newBuilder()
.host(nodeAddress)
.port(nodePort)
.useTLS(TLSConfig.auto())
.build()
val client = ClientV2.from(connection)
val cryptographicParameters = client.getCryptographicParameters(BlockQuery.BEST)
val seedPhrase = "fence tongue sell large master side flock bronze ice accident what humble bring heart swear record valley party jar caution horn cushion endorse position"
@OptIn(ExperimentalStdlibApi::class)
val seedAsHex = Mnemonics.MnemonicCode(seedPhrase.toCharArray()).toSeed().toHexString()
val wallet = ConcordiumHdWallet.fromHex(seedAsHex, Network.TESTNET) // Or Network.MAINNET, if working on mainnet.
val identityProviderIndex = identityProvider.ipInfo.ipIdentity.value
val idCredSec = wallet.getIdCredSec(identityProviderIndex, identityIndex)
val prfKey = wallet.getPrfKey(identityProviderIndex, identityIndex)
val blindingRandomness = wallet.getSignatureBlindingRandomness(identityProviderIndex, identityIndex)
val arThreshold = (identityProvider.arsInfos.size - 1).coerceAtMost(255)
val input: IdentityRequestInput = IdentityRequestInput.builder()
.globalContext(cryptographicParameters)
.ipInfo(identityProvider.ipInfo)
.arsInfos(identityProvider.arsInfos)
.arThreshold(arThreshold.toLong())
.idCredSec(idCredSec)
.prfKey(prfKey)
.blindingRandomness(blindingRandomness)
.build()
return Identity.createIdentityRequest(input)
}
import Concordium
import Foundation
// Inputs.
let seedPhrase = "fence tongue sell large master side flock bronze ice accident what humble bring heart swear record valley party jar caution horn cushion endorse position"
let network = Network.testnet
let identityProviderID = IdentityProviderID(3)
let identityIndex = IdentityIndex(7)
let walletProxyBaseURL = URL(string: "https://wallet-proxy.testnet.concordium.com")!
let anonymityRevocationThreshold = RevocationThreshold(2)
/// Perform an identity creation based on the inputs above.
func createIdentity(client: NodeClient) async throws {
let seed = try decodeSeed(seedPhrase, network)
let walletProxy = WalletProxy(baseURL: walletProxyBaseURL)
let identityProvider = try await findIdentityProvider(walletProxy, identityProviderID)!
// Construct identity creation request and start verification.
let cryptoParams = try await client.cryptographicParameters(block: .lastFinal)
let identityReq = try issueIdentitySync(seed, cryptoParams, identityProvider, identityIndex, anonymityRevocationThreshold) { issuanceStartURL, requestJSON in
// The URL to be invoked when once the ID verification process has started (i.e. once the data has been filled in).
let callbackURL = URL(string: "concordiumwallet-example://identity-issuer/callback")!
let urlBuilder = IdentityRequestURLBuilder(callbackURL: callbackURL)
let url = try urlBuilder.issuanceURLToOpen(baseURL: issuanceStartURL, requestJSON: requestJSON)
todoOpenURL(url)
return todoAwaitCallbackWithVerificationPollingURL()
}
let res = try await todoFetchIdentityIssuance(identityReq)
if case let .success(identity) = res {
print("Identity issued successfully: \(identity))")
} else {
// Verification failed...
}
}
func issueIdentitySync(
_ seed: WalletSeed,
_ cryptoParams: CryptographicParameters,
_ identityProvider: IdentityProvider,
_ identityIndex: IdentityIndex,
_ anonymityRevocationThreshold: RevocationThreshold,
_ runIdentityProviderFlow: (_ issuanceStartURL: URL, _ requestJSON: String) throws -> URL
) throws -> IdentityIssuanceRequest {
print("Preparing identity issuance request.")
let identityRequestBuilder = SeedBasedIdentityRequestBuilder(
seed: seed,
cryptoParams: cryptoParams
)
let reqJSON = try identityRequestBuilder.issuanceRequestJSON(
provider: identityProvider,
index: identityIndex,
anonymityRevocationThreshold: anonymityRevocationThreshold
)
print("Start identity provider issuance flow.")
let url = try runIdentityProviderFlow(identityProvider.metadata.issuanceStart, reqJSON)
print("Identity verification process started!")
return .init(url: url)
}
func todoOpenURL(_: URL) {
// Open the URL in a web view to start the identity verification flow with the identity provider.
fatalError("'openURL' not implemented")
}
func todoAwaitCallbackWithVerificationPollingURL() -> URL {
// Block the thread and wait for the callback URL to be invoked (and somehow capture that event).
// In mobile wallets, the callback URL is probably a deep link that we listen on somewhere else.
// In that case, this snippet would be done now and we would expect the handler to be eventually invoked.
// In either case, the callback is how the IP hands over the URL for polling the verification status -
// and for some reason it does so in the *fragment* part of the URL!
// See 'server.swift' of the example CLI for a server-based solution that works in a synchronous context.
// Warning: It ain't pretty.
fatalError("'awaitCallbackWithVerificationPollingURL' not implemented")
}
func todoFetchIdentityIssuance(_ request: IdentityIssuanceRequest) async throws -> IdentityVerificationResult {
// Block the thread, periodically polling for the verification status.
// Return the result once it's no longer "pending" (i.e. the result is non-nil).
while true {
let status = try await request.send(session: URLSession.shared)
if let r = status.result {
return r
}
try await Task.sleep(nanoseconds: 10 * 1_000_000_000) // check once every 10s
}
}
Send an identity request#
Once the identity request has been created, the next step is to send it to the corresponding identity provider. There are multiple ways to accomplish this, and it will depend on the technologies you choose. Below is an example of how it can be done.
A part of the request is a redirectUri, which tells the identity provider where to redirect the user when the identity verification flow has been completed. A wallet application has to choose this in such a way that the user is sent back into the wallet application, where the actual identity object can then be retrieved from the information provided in the hash property of the redirect URL.
import {
IdObjectRequestV1,
Versioned,
} from '@concordium/web-sdk';
// The identity provider that the request was created for.
const identityProvider: IdentityProviderWithMetadata = ...;
const identityIssuanceStartUrl = identityProvider.metadata.issuanceStart;
// The identity request created in the previous step.
const identityRequest: Versioned<IdObjectRequestV1> = ...;
// This value determines where the identity provider will redirect the user
// at the end of the identity verification process. This can e.g. be to a deep link
// that your application listens for, so that your application is automatically activated
// again.
const redirectUri = 'some-custom-value';
const params = {
scope: 'identity',
response_type: 'code',
redirect_uri: redirectUri,
state: JSON.stringify({ identityRequest }),
};
const searchParams = new URLSearchParams(params);
const url = `${identityIssuanceStartUrl}?${searchParams.toString()}`;
const response = await fetch(url);
// The identity creation protocol dictates that we will receive a redirect.
// If we don't receive a redirect, then something went wrong at the identity
// provider's side.
if (!response.redirected) {
throw new Error('The identity provider did not redirect as expected.');
} else {
// The response URL contains the location that the user should be redirected to,
// e.g. by opening it in a browser. This will start the identity verification at
// the identity provider.
return response.url;
}
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import okhttp3.OkHttpClient
import okhttp3.Request
fun sendIdentityRequest(context: Context, identityProvider: IdentityProvider, identityRequest: String) {
// This value determines where the identity provider will redirect the user
// at the end of the identity verification process. This can e.g. be to a deep link
// that your application listens for, so that your application is automatically activated
// again.
val redirectUri = "yourwallet-scheme://identity-issuer/callback"
val baseUrl = identityProvider.metadata.issuanceStart
val delimiter = if (baseUrl.contains('?')) "&" else "?"
val url = "${baseUrl}${delimiter}response_type=code&redirect_uri=${redirectUri}&scope=identity&state=$identityRequest"
val okHttpClientBuilder = OkHttpClient().newBuilder().followRedirects(false).followSslRedirects(false)
val client = okHttpClientBuilder.build()
val request = Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
// The identity creation protocol dictates that we will receive a redirect.
// If we don't receive a redirect, then something went wrong at the identity
// provider's side.
// The redirected URL contains the location that the user should be redirected to,
// e.g. by opening it in a browser. This will start the identity verification at
// the identity provider.
val redirectedUrl = response.header("Location")
?: throw Exception("The identity provider did not redirect as expected.")
// Open the URL in a browser. This is just an example of how that could be done.
val customTabsIntent = CustomTabsIntent.Builder().build()
customTabsIntent.launchUrl(context, Uri.parse(redirectedUrl))
}
}
The Swift SDK for iOS is still in development.
Retrieve the identity after creation#
Upon completing identity verification with the identity provider, the identity provider does a redirect of the user back to the redirectUri that was provided when sending the identity request to the identity provider. The hash property of the URL that the identity provider redirects the user to contains the URL where the identity object can be retrieved from in the format redirectUri#code_uri=, where the URL will be after the equals sign.
enum IdentityProviderIdentityStatus {
/** Pending identity verification. */
Pending = 'pending',
/** The identity creation failed or was rejected. */
Error = 'error',
/** The identity is ready. */
Done = 'done',
}
interface PendingIdentityTokenContainer {
status: IdentityProviderIdentityStatus.Pending;
detail: string;
}
interface DoneIdentityTokenContainer {
status: IdentityProviderIdentityStatus.Done;
token: { identityObject: Versioned<IdentityObjectV1> };
detail: string;
}
interface ErrorIdentityTokenContainer {
status: IdentityProviderIdentityStatus.Error;
detail: string;
}
type IdentityTokenContainer =
| PendingIdentityTokenContainer
| DoneIdentityTokenContainer
| ErrorIdentityTokenContainer;
// The URL that the identity provider redirected to when the user completed
// identity verification.
const identityProviderRedirectUrl: string = ...;
// Extract the location where the identity can be retrieved from.
const identityUrl = identityProviderRedirectUrl.split('#code_uri=')[1];
try {
const response = (await (await fetch(identityUrl)).json as IdentityTokenContainer;
if (IdentityProviderIdentityStatus.Done === response.status) {
// The identity is ready and can be extracted and stored locally
// in the user's wallet.
const identity: IdentityObjectV1 = response.token.identityObject.value;
} else if (IdentityProviderIdentityStatus.Error === response.status) {
// Something went wrong and the details about the error are available.
const errorDetails: string = response.detail;
} else {
// In this case the identity is still pending, and the identity
// should be queried again after some time to check the status again.
// An identity will always resolve to either the done status or the
// error status.
}
} catch {
// Something went wrong while querying the identity provider for the identity.
// The wallet should retry after some time if this happens.
}
import com.concordium.sdk.crypto.wallet.identityobject.IdentityObject
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import okhttp3.OkHttpClient
import okhttp3.Request
@JsonAutoDetect
private data class VersionedIdentity(
val v: Number,
val value: IdentityObject
)
private data class IdentityWrapper(val identityObject: VersionedIdentity)
private data class IdentityResponse(
val status: Status,
val token: IdentityWrapper?,
val detail: String?,
) {
enum class Status {
@JsonProperty("done")
DONE,
@JsonProperty("pending")
PENDING,
@JsonProperty("error")
ERROR,
}
}
fun fetchIdentity() {
// The URL that the identity provider redirected to when the user completed
// identity verification.
val uri = ...
val identityUri = uri.split("#code_uri=").last()
val request = Request.Builder().url(identityUri).build()
val httpClient = OkHttpClient().newBuilder().build()
httpClient.newCall(request).execute().use { response ->
response.body()?.use { body ->
val mapper = jacksonObjectMapper()
val identityResponse = mapper.readValue(body.string(), IdentityResponse::class.java)
if (IdentityResponse.Status.DONE == identityResponse.status) {
// The identity is ready and can be extracted and stored locally
// in the user's wallet.
val identity: IdentityObject = identityResponse.token!!.identityObject.value
} else if (IdentityResponse.Status.ERROR == identityResponse.status) {
// Something went wrong and the details about the error are available.
val errorDetails = identityResponse.detail
} else {
// In this case the identity is still pending, and the identity
// should be queried again after some time to check the status again.
// An identity will always resolve to either the done status or the
// error status.
}
}
}
}
The Swift SDK for iOS is still in development.