In the FileKit and Core tutorial, you were briefly introduced to the concept of identities. They were managed by a server hosted by Tanker called FakeAuthentication. In this tutorial you will build your own identity management server.

After having completed this tutorial, you will know:

  • How to install the Identity SDK in your server
  • How to generate identities for your users
  • How to extract public identities for sharing
  • How to validate user account creation

1. Introduction to identities

To share encrypted resources with users, Tanker needs to have a way to identify the recipients.

Identities fulfill this role, but they contain sensitive data, and thus must not be accessed by other users.

Their non-sensitive counterparts are public identities. They are used to represent recipients.

2. Create a Node.js app with express

mkdir identity-server
cd identity-server
yarn init
yarn add express body-parser

Let's create a Server.js file, containing a simple route, and a middleware that parses JSON:

File: Server.js
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const port = 3001; app.use(bodyParser.json()); app.post('/identity', (req, res) => res.send('Hello, you are on the identity route')); app.listen(port, () => console.log(`Identity server listening on port ${port}!`));

You can start this server:

yarn start

And test the route:

curl -X POST localhost:3001/identity
Hello, you are on the identity route

3. Integrate your Tanker app

Warning

You will need a Tanker account to follow the rest of the tutorial. If you don't have one, please sign up on dashboard.tanker.io

First you need a Tanker app. You can create one in the dashboard. Make sure to disable the test mode, and save the credentials: the app ID and the app secret.

File: Server.js
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); const port = 3001; const appID = 'YOUR_APP_ID'; const appSecret = 'YOUR_APP_SECRET'; app.use(bodyParser.json()); app.post('/identity', (req, res) => res.send('Hello, you are on the identity route'));

To generate identities you need the @tanker/identity package. We will also need uuid to generate unique user IDs:

yarn add @tanker/identity uuid

Now you can generate an identity when the route is called:

File: Server.js
const express = require('express'); const bodyParser = require('body-parser'); const uuid = require('uuid/v4'); const tanker = require('@tanker/identity'); const app = express(); const port = 3001; const appID = 'YOUR_APP_ID'; const appSecret = 'YOUR_APP_SECRET'; app.use(bodyParser.json()); app.post('/identity', (req, res) => res.send('Hello, you are on the identity route')); app.post('/identity', async (req, res) => { const id = uuid(); const tankerIdentity = await tanker.createIdentity(appID, appSecret, id); const user = { id, tankerIdentity, } res.json(user); }); app.listen(port, () => console.log(`Identity server listening on port ${port}!`));

Let's test it:

curl -X POST localhost:3001/identity
{"id":"ced14285-9816-4fd8-9025-032211427371","tankerIdentity":"..."}

This route indeed serves an identity. However, it will never serve the same identity twice because it creates a new identity every time it is called. In a real-life application, we want to save the generated identity so that we can serve it again when needed. In this tutorial we will use the email address to identify a particular user.

4. Map user to identity

Let's keep a map of the users in memory. We create the unique user ID and the corresponding identity, only if the email is not in the map.

Note

This system does not provide persistent data, in a real-life application you will need to save the identity in you users database

File: Server.js
const app = express(); const port = 3001; const appID = 'YOUR_APP_ID'; const appSecret = 'YOUR_APP_SECRET'; const users = {}; app.use(bodyParser.json()); app.post('/identity', async (req, res) => { const id = uuid(); const tankerIdentity = await tanker.createIdentity(appID, appSecret, id); const user = { id, tankerIdentity, const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } const user = users[email] || {}; if (!user.tankerIdentity) { user.id = uuid(); user.tankerIdentity = await tanker.createIdentity(appID, appSecret, user.id); users[email] = user; } res.json(user); }); app.listen(port, () => console.log(`Identity server listening on port ${port}!`));

Let's test it:

curl -X POST --data '{"email": "bob@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"id":"c46bcbe5-b61b-42a0-9826-6125e23b08ab","tankerIdentity":"..."}

Now we ask for an identity with the same email address:

curl -X POST --data '{"email": "bob@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"id":"c46bcbe5-b61b-42a0-9826-6125e23b08ab","tankerIdentity":"..."}

For a same email input, we have the same id and tankerIdentity.

5. Identify a user for others

The identity is only distributed to an authenticated user to start their Tanker session. To share data with another user, you need their public identity. Let's create another route to retrieve this public identity:

File: Server.js
app.post('/public_identity', async (req, res) => { const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } let identity; const user = users[email] || {}; if (user.tankerIdentity) { identity = user.tankerIdentity; } else { return res.status(400).json({ error: 'unknown user' }); } res.json({ publicIdentity: await tanker.getPublicIdentity(identity) }); }); app.listen(port, () => console.log(`Identity server listening on port ${port}!`));

Let's test it:

  • Bob registers
curl -X POST --data '{"email": "bob@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"id":"88d94323-94f8-4e65-975c-ac9f1871d043","tankerIdentity":"..."}
  • Alice registers
curl -X POST --data '{"email": "alice@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"id":"031d4f3c-fab3-405c-9e88-44f9976fa041","tankerIdentity":"..."}
  • Alice wants Bob's public identity to share data with him
curl -X POST --data '{"email": "bob@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"publicIdentity":"..."}

6. What about unregistered users ?

In a file transfer application, you upload your files and share them using the email address of the recipient, without knowing if this user has registered to the application yet. In this case, the user does not have an identity.

If we try to retrieve the identity of an unregistered user with the current server:

curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"error":"unknown user"}

For the moment, the server returns an error. However, Tanker has been designed with this use-case in mind. So far, we've only be looking at permanent identities. Let's explore provisional identities as well.

When a user does not exist in our server, we create a provisional identity for this user:

File: Server.js
app.post('/public_identity', async (req, res) => { const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } let identity; const user = users[email] || {}; if (user.tankerIdentity) { identity = user.tankerIdentity; } else if (user.provisionalIdentity) { identity = user.provisionalIdentity; } else { return res.status(400).json({ error: 'unknown user' }); identity = await tanker.createProvisionalIdentity(appID, email); users[email] = { provisionalIdentity: identity }; } res.json({ publicIdentity: await tankerIdentity.getPublicIdentity(identity) }); });

Let's test it:

  • Try to get Charlie's public identity, the server creates a provisional identity, and returns a public identity corresponding to this provisional identity:
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"publicIdentity":"..."}
  • Charlie finally registers, the server creates a permanent identity and serves both the permanent identity and the provisional identity:
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"provisionalIdentity":"...","id":"d6267f30-59d6-4ddb-9188-3865b84463e1","tankerIdentity":"..."}

You can now attach this provisional identity to the permanent identity in the Client SDK.

7. Validate an identity

In some cases, the permanent identity has been generated by your server, but has not been registered client-side, for instance if the user did not confirm their email address. In this case, if another user wants to share data with this user they should use the provisional identity.

To make sure that the permanent identity has been registered client-side, we will use a boolean set to true by a new route:

File: Server.js
app.post('/identity', async (req, res) => { const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } const user = users[email] || {}; if (!user.tankerIdentity) { user.id = uuid(); user.tankerIdentity = await tankerIdentity.createIdentity(appID, appSecret,user.id); user.registered = false; users[email] = user; } res.json(user); }); app.patch('/identity', async (req, res) => { const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } if (!(email in users)) { return res.status(400).json({ error: 'unkwnown user' }); } const user = users[email]; user.registered = true; users[email] = user; res.sendStatus(200); }); app.post('/public_identity', async (req, res) => { const email = req.body.email; if (!email) { return res.status(400).json({ error: 'no email provided' }); } let identity; const user = users[email] || {}; if (user.tankerIdentity) { if (user.tankerIdentity && user.registered) { identity = user.tankerIdentity; } else if (user.provisionalIdentity) { identity = user.provisionalIdentity; } else { identity = await tankerIdentity.createProvisionalIdentity(appID, email); users[email] = { provisionalIdentity: identity }; } res.json({ publicIdentity: await tankerIdentity.getPublicIdentity(identity) }); });

Let's test:

  • Charlie does not have a permanent identity yet, a provisional identity is created, and a public identity corresponding to this provisional identity is returned:
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"publicIdentity":"PUBLIC_IDENTITY_1"}
  • Charlie asks for a permanent identity
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
{"provisionalIdentity":"PROVISIONAL_IDENTITY","id":"765097ec-e333-4d61-84b3-f6d32ec6c093","tankerIdentity":"TANKER_IDENTITY"}
  • Charlie does not register his permanent identity, someone asks for his public identity. The public identity corresponding to his provisional identity is returned:
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"publicIdentity":"PUBLIC_IDENTITY_1"}
  • Charlie finally registers his permanent identity:
curl -X PATCH --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/identity
OK
  • Someone asks for his public identity, the public identity corresponding to his permanent identity is returned
curl -X POST --data '{"email": "charlie@tanker-tuto.io"}' -H "Content-Type: application/json" localhost:3001/public_identity
{"publicIdentity":"PUBLIC_IDENTITY_2"}

You now have a fully functional Tanker identity management server.