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 logincancelUrl
: 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.
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.
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.