Skip to main content

Single Sign-On (SSO)

SSO allows user to login to different dapps with a single account. It's similar to how you can click a "Login with Facebook/Google" buttons to login to different services using a single Facebook/Google account.

In order for SSO to work, your dapp must integrate with a SSO service. Chromia Vault is the main SSO service in Chromia ecosystem, though a custom SSO service can be easily implemented thanks to FT3 module's flexible authentication model.

As discussed in previous sections, access to a FT3 account gets controlled with authentication descriptors Account management. So if we use a Vault account's public key to create an authDescriptor with 'A' flag, and add it to a dapp account. Vault has control over the dapp account.

This is a three-step process:

  • User login to their Vault account, and approve of the SSO request.
  • Vault's keyPair gets used to create a new account for dapp (so dapp account and Vault account have the same accountId). A second authDescriptor with only 'T' flag gets created on-the-fly and added to this new dapp account.
  • The user can now use the second keyPair to perform transactions for the account.

This keyPair is disposable and you can safely discard or replace it.

This might sound rather complicated to implement, but fortunately ft3-lib library already handle all of the heavy works, and dapps only have to config the SSO Class a bit for it to work.

The bootstrap project already contains a client directory that implements the SSO flow. First, you set up the flow using bootstrap client, then you need to configure SSO flow for your client.

SSO flow with bootstrap client

Backup your client directory somewhere safe, and replace it with the client directory from bootstrap project.

Set config variables

Bootstrap client use dotenv npm package to set config variables. In client directory you can see a .env.sample file. Duplicate the file and rename it to .env. Update the file with your chain's settings, be sure to uncomment those lines (remove leading #):

REACT_APP_VAULT_URL=https://vault-testnet.chromia.com
REACT_APP_BLOCKCHAIN_RID=DAPP_BLOCKCHAIN_RID
REACT_APP_NODE_ADDRESS=http://localhost:7743

Set up the dapp on Vault

Because your dapp isn't public on the Chain Explorer yet, you need to configure it for testing as a custom dapp.

Go to the Vault page and login to your account (Follow the instruction at Chromia Vault section if you are unsure).

Scroll down to the "All DApps" section, and click "Add Custom DApp". Fill in information of your chain (similar to how you did with Chain Explorer during project setup).

You see your dapp tile added to the Vault's dapp list.

Now the SSO flow is ready to use. Go back to your dapp (click the tile in Vault's dapp list), and you see a login screen. Clicking the login button must redirect you to Vault to login to your Vault account.

If everything was setup correctly, Vault must ask you to authorize dapp. Clicking "Authorize" button must redirect you back to your client, with the newly created account's information displayed.

If the Authorize page isn't displayed, it indicates a problem with your configs. Verify that the BRID and host is correct and try again.


We got the bootstrap client to work with SSO. You can use the bootstrap client as a base to build your own client. But if you want to implement SSO in your own client, the next part discuss how to do exactly that.

SSO class

FT3 provides SSO capability through SSO class. The first step in integrating SSO into a dapp is to initialize SSO class on the app launch:

import { Blockchain, SSO } from "ft3-lib";
import { blockchainRID, blockchainUrl, vaultUrl } from "./configs/constants"; // these configs are set in project-setup section

const chainId = Buffer.from(blockchainRID, "hex");
const blockchain = await new Postchain(blockchainUrl).blockchain(chainId);

SSO.vaultUrl = vaultUrl;
const sso = new SSO(blockchain);

Initiate login

When user click the "Login with Vault" button, dapp should call initiateLogin:

const { SSO } from 'ft3-lib';

const successUrl = `${window.location.origin}/success`;
const cancelUrl = `${window.location.origin}/cancel`;

sso.initiateLogin(successUrl, cancelUrl);

initiateLogin navigates a user to the Vault, where they have to select one of their accounts to create a new dapp account or login to existing dapp account. After the user logs in to their vault account and authorizes login or account creation, they get redirected back to the dapp to finalize login.

  • successUrl: where to redirect to after a successful login
  • cancelUrl: where to redirect to when user cancels the login

Finalize login

If there were no errors, Vault redirects the user to the location of successUrl.

Vault adds query parameters containing raw transaction - which dapp needs to sign and post to the blockchain.

finalizeLogin method handles singing and posting as follows:

import { parse } from "query-string";

const { rawTx } = parse(search); // extract rawTx query parameter

try {
const [account, user] = await sso.finalizeLogin(rawTx);
} catch (error) {
// handle error
}

finalizeLogin returns a tuple which contains ft3 account (Account instance) and ft3 user (User instance).

If an account doesn't already exist, call to finalizeLogin creates it, and if it was already created, only auth descriptor would get added to the account. Returned user object contains a key pair used to sign the transactions and an auth descriptor to authorize operations.

The same flow is applicable for registering new dapp account and login to existing account.

Auto-login (remember me)

By default, SSO uses an instance of SSOStoreDefault to keep track of account (account id) and user (key pair) details in memory. As soon as SSO instance gets destroyed (for example, page refresh) the account and user details disappear.

But it's also possible to persist them to local storage. If you initialize SSO with SSOStoreLocalStorage, the details get stored to loocal storage:

import { SSO, SSOStoreLocalStorage } from "ft3-lib";

const sso = new SSO(blockchain, new SSOStoreLocalStorage());

On app launch, you can use the auto-login feature to auto login user if they have already logged in previously:

const [account, user] = await sso.autoLogin();

if (account === null) {
// redirect to login page
}

Like finalizeLogin method, autoLogin returns account and user objects.

Auto login works only if SSO can find account id and keypair in LocalStorage, and if the authDescriptor which corresponds to stored key pair didn't expire.

If autoLogin returns null for account and user, then user must get redirected to a normal login page, where initiateLogin must get called to allow login using the Vault.

caution

Storing the key pair inside LocalStorage is potentially a security risk, therefore you must use SSOStoreLocalStorage only if dapp doesn't require a high level of security. In high security cases, dapp should implement its own method of preserving user.keyPair for future use.

Logout

Call logout method to delete local cache and auth descriptor:

await sso.logout();

xxxxxxxxxx query get_asset_balances(account_id: byte_array)​query get_asset_balance(account_id: byte_array, asset_id: byte_array)​query get_asset_by_name(name)​query get_asset_by_id(asset_id: byte_array)​query get_all_assets()​query get_payment_history(account_id: byte_array, after_block: integer)rell

Low level details

As briefed in the beginning, SSO flow creates an ft3 account on a dapp's chain which has the same accountId as the user's Vault account.

In the flow, Vault is responsible for building a transaction which creates the account and adds an authDescriptor to it. Once Vault passes the transaction to the dapp (via request parameters), dapp signs it and posts it to the blockchain by calling finalizeLogin.

This transaction performs two operations, which add two auth descriptors to the account. One gets added with a call to register_account operation and second gets added with add_auth_descriptor operation.

  • First authDescriptor gets created using Vault public key and has 'A' and 'T' flags
  • The second gets created using a disposable public key (generated by initializeLogin method) with only "T" flag.

If an account with the same accountId as Vault's account already exists on the blockchain, SSO only adds the second authDescriptor with disposable public key.

info

Because the new account need to add the disposable auth descriptor immediately at creation time, you must set a value equal or greater than 1 for rate_limit_points_at_account_creation in config.template.xml as noted during Project Setup.

If you also need to record additional user information (as discussed below), you need to increase the minimum value further.

Dapp account when using SSO

SSO can only create a ft3 account, but in most cases that's not enough to store dapp account details.

Using the same example dapp_account entity in Rell Integration:

entity dapp_account {
key account: ft3.account;
key username: text;
}

We can make some changes to support the SSO flow:

operation create_dapp_account(
username: text,
account_id: byte_array,
auth_descriptor_id: byte_array
) {
val account = auth_and_log(
account_id,
auth_descriptor_id,
list<text>()
);

create dapp_account (account, username);
}

query get_dapp_account(account_id: byte_array) {
return dapp_account @? { .account.id == account_id } (
account_id = .account.id,
username = .username
);
}

And then on the client side the login flow would look like this:

const [account, user] = await blokchain.finalizeLogin(tx);

const dapp_account = await blockchain.query("get_dapp_account", {
account_id: account.id,
});

const username = "john_doe"; // Fake username

if (!dapp_account) {
await blockchain.call(
op("create_dapp_account", username, account.id, user.authDescriptor.id),
user
);
}

On the login success page, we verify if the dapp account exists by calling get_user query. If it returns null then it means the account doesn't exist yet, so you need to create it.

In this oversimplified example, it's done by calling create_user operation with hard-coded info. In a real dapp, we would need a form where user can enter those account details.

Next time the user logs in, get_user query returns the dapp account, so the dapp account creations step gets skipped.