How to Authenticate Callers with Twilio, CIBA, and the Swedish BankID Phone Authentication
In this tutorial, we’ll create a minimal implementation of Twilio’s Voice API along with Swedish BankID in telephone calls to demonstrate simple and secure user verification during phone calls.
The use case
Imagine your business operates a call center where it's essential to confirm caller identities.
Instead of relying on outdated security questions like the mother's maiden name, wouldn't it be better to authenticate callers in real-time using just their smartphone and a dedicated app?
Fortunately, the new OpenID Connect Client Initiated Backchannel Authentication (CIBA) flow was designed for this very purpose.
Read all about CIBA in a dedicated blog post.
The implementation
Criipto Verify recently introduced support for Swedish BankID in telephone calls (BankID Phone Authentication) through CIBA. So we can leverage secure and trusted Swedish BankID to authenticate users over the phone.
As for creating our own “call center” that can be managed programmatically, Twilio is the perfect counterpart. Twilio offers tools for making and receiving phone calls through easy-to-use APIs – exactly what we need to showcase our idea.
Here is the workflow we’ll build:
- A user calls the designated "call center" number managed by Twilio.
- During the call, the user is prompted to enter their Swedish Social Security Number(SSN).
- Once SSN is provided, the user receives a notification in their Swedish BankID app, asking them to authenticate.
- Upon successful authentication, the “call center” can provide personalized information to the user.
Feel free to follow along. As you go through the tutorial, you'll make calls to the Twilio-managed "call center” and authenticate with the Swedish BankID app.
Let’s get started!
Required accounts and tools
Here is what we need to complete this tutorial:
- Node.js: We'll be creating a Node.js application, so ensure you have Node.js installed on your machine. You can download the latest version from the official website.
- Twilio account and phone number: Sign up for an account at Twilio. Then, choose and configure a Twilio phone number.
- Ngrok: Ngrok will enable tunneling of our local server to the Internet, allowing Twilio to talk to our application. You can install Ngrok from the official website.
- Criipto account: Sign up for a free developer account at Criipto, then create your first domain and application.
- Swedish BankID test application and test user: These instructions will point you in the right direction.
Once everything is ready, let's get started with our implementation.
Set up your development environment
Initialize a new Node.js project
Create a new directory for your project and navigate into it:
mkdir twilio-ciba-auth
cd twilio-ciba-auth
Initialize a new Node.js project:
npm init -y
Install required dependencies
We need:
- express: to create a server and handle routing.
- body-parser: to parse incoming request bodies.
- axios: for making requests to external APIs; will be used for interactions with Twilio.
Install packages by running:
npm install express body-parser axios
Implement BankID Phone Authentication
Let's start by writing the code to authenticate a user via Swedish BankID Phone Auth. This code will trigger BankID app authentication for a specific user. We’ll later integrate this part to handle authentication during phone calls.
Review the full documentation for more detailed information about each step.
Create a file named phone_auth.js. Then, require the axios package we previously installed, and add your Criipto credentials:
const axios = require('axios');
const domain = {YOUR_CRIIPTO_DOMAIN};
const clientId = {CLIENT_ID_OF_YOUR_CRIIPTO_APPLICATION};
const clientSecret = {CLIENT_SECRET_OF_YOUR_CRIIPTO_APPLICATION};
We’re using clientId and clientSecret for client authentication to keep it simple. We recommend using Private Key JWTs whenever possible.
Write a function to send an HTTP POST request to the /ciba/bc-authorize endpoint at your domain to initiate the authentication process:
const startAuth = async (ssn) => {
try {
const response = await axios.post(
`https://${domain}/ciba/bc-authorize`,
new URLSearchParams({
scope: 'openid',
callInitiator: 'user', // We expect the user to initiate the call
login_hint: `sub:ssn:${ssn}`, // User’s SSN number
acr_values: 'urn:grn:authn:se:bankid', // acr_values for the Swedish BankID
binding_message: "Hello from Twilio, CIBA and the Swedish Phone Auth Tutorial!", // Message displayed in the app
}).toString(),
{
headers: {
Authorization: 'Basic ' + Buffer.from(`${encodeURIComponent(clientId)}:${clientSecret}`).toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return response.data;
} catch (error) {
console.error('Error in auth:', error.message);
throw error;
}
};
A successful response will contain the request ID (auth_req_id) and look something like this:
{
"auth_req_id" : "3857f8ff-21b9-48ae-a732-a3bd8128a7ae",
"expires_in" : 120
}
Next, use the auth_req_id to poll the token endpoint (/oauth2/token) for a response:
const poll = async (auth_req_id) => {
try {
const response = await axios.post(
`https://${domain}/oauth2/token`,
new URLSearchParams({
auth_req_id: auth_req_id,
grant_type: 'urn:openid:params:grant-type:ciba',
}).toString(),
{
headers: {
Authorization: 'Basic ' + Buffer.from(`${encodeURIComponent(clientId)}:${clientSecret}`).toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return response.data;
} catch (error) {
if (error.response && error.response.status === 400 && error.response.data.error === 'authorization_pending') {
// 'authorization_pending' error is expected, ignore it
return null;
}
console.error('Error in poll:', error.message);
throw error;
}
};
Finally, create a function to manage the entire authentication flow, polling for token every 5 seconds:
const authenticate = async (ssn) => {
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
try {
const authResponse = await startAuth(ssn);
const auth_req_id = authResponse.auth_req_id;
for (let i = 0; i < 10; i++) {
try {
const response = await poll(auth_req_id);
if (response) {
console.log('Response:', response);
if (response.id_token) {
console.log('ID Token:', response.id_token);
return response;
}
}
} catch (e) {
console.error('Error during polling:', e.message);
}
await delay(5000);
}
throw new Error('Authentication failed.');
} catch (e) {
console.error('Error in authentication flow:', e.message);
}
};
Invoke the function with the SSN number of your test user:
authenticate('196802020575');
Then head to your terminal and execute the code:
node phone_auth.js
Open the Swedish BankID test app on your smartphone. A test user (whose SSN we just used) should get a Security check message:
Select Yes, then authenticate in the app.
In a moment, you should see the authentication result in your terminal:
Response: {
token_type: 'Bearer',
expires_in: '120',
id_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiVBQzZG….my1R4gXx_S-34KkIgFn3cr',
access_token: '938fbcbf-b4e0-4818-80f3-e8714203980c'
}
The ID Token from successful authentication contains JWT claims with personal details of our test user, just as expected.
Finally, remove the function call and export the authentication function:
module.exports = main;
With the caller authentication set up, let’s introduce Twilio into our application.
Configuring Twilio
Twilio is a cloud-based communication platform that provides APIs and tools to enable businesses to integrate voice, messaging, and video capabilities into their applications and websites, eliminating the need to build and maintain a complex communication infrastructure.
In this tutorial, we’ll use Twilio's Programmable Voice and Node.js SDK.
Twilio Basics
Before diving into handling calls, let’s quickly cover some basic concepts:
Twilio’s REST API
Twilio’s REST API lets you programmatically send and receive calls and SMS messages via HTTP requests. It supports various programming languages and libraries. Programmable Voice is Twilio’s API for handling phone calls.
Twilio webhooks
Webhooks are an essential component of Twilio’s functionality. A webhook is a URL that Twilio will send HTTP requests to whenever your Twilio phone number receives a call. Webhooks are responsible for receiving and processing these requests.
The TwiML format
When Twilio sends an HTTP request to your application, it expects a response in TwiML (Twilio Markup Language), an XML-based format that tells Twilio how to respond to the call.
Write Node.js code to respond to incoming phone calls
To handle incoming phone calls, we'll need our web application to accept HTTP requests from Twilio.
When your Twilio number receives a call, Twilio will send an HTTP POST request to your webhook endpoint, expecting instructions on how to handle the call. Your web application will respond with a TwiML document containing these instructions (e.g. to say some text, gather user input, and more).
Back in our app, install Twiilo’s Node.js SDK by running:
npm install twilio
Next, create a file called get-call.js, and include the necessary dependencies:
- express for the web server,
- urlencoded from body-parser to parse incoming requests
- VoiceResponse from twilio for generating TwiML responses
const express = require('express');
const VoiceResponse = require('twilio').twiml.VoiceResponse;
const urlencoded = require('body-parser').urlencoded;
const app = express();
app.use(urlencoded({ extended: false }));
// Handle incoming calls
app.post('/twilio/webhook/init', (req, res) => {
res.type('xml');
const twiml = new VoiceResponse();
twiml.say('Hello from the server!');
// Send the TwiML response
res.send(twiml.toString());
});
// Start the server
app.listen(1337, '127.0.0.1');
console.log('TwiML server running at http://127.0.0.1:1337/');
In this example, the Express server listens for incoming POST requests from Twilio on port 1337, and routes them to '/twilio/webhook/init'. When a request is received, the server responds with TwiML that instructs Twilio (using Twilio's <Say> verb) to say "Hello from the server!" during the call.
You can now run the application in your terminal:
node get-call.js
To learn more about handling incoming calls, check Twilio’s comprehensive tutorial and quickstart guide for Node.js.
Make your application accessible to the Internet
To enable Twilio to send HTTP requests to your web application, the application must be accessible over the Internet with a public URL or IP address. During development, you can use ngrok to create a public URL for your local server.
Once ngrok is installed and our application is running locally, open a new terminal window and start ngrok with this command:
ngrok http 1337
This will create a public URL for our web application listening on port 1337:
Copy the URL from the output and paste it into your browser.
You should see your Node.js application's "Hello from the server!" message.
Configure webhook in the Twilio Console
Head to the Twilio console and navigate to your phone number. In the Voice Configuration section, change "A call comes in” to "Webhook", and add your ngrok public URL (remember to append the URL path: e.g. https://<your_ngrok_subdomain>.ngrok-free.app/twilio/webhook/init in our example).
Click "save" and head back to the terminal. Make sure both ngrok and your Node.js app are still up and running.
Now, you can call your Twilio phone number.
In a moment, you'll see an HTTP request in your ngrok console and hear a voice message once the call connects.
Excellent! Your Node.js setup now handles phone calls.
Update Node.js code to gather user’s input
Let’s add the code to prompt a user to input their SSN number.
Replace the existing code in the ‘/twilio/webhook/init’ route:
app.post('/twilio/webhook/init', (req, res) => {
res.type('xml');
const twiml = new VoiceResponse();
//twiml.say('Hello from the server!');
const gather = twiml.gather({
input: 'dtmf',
action: '/twilio/webhook/authenticate,
finishOnKey: '#',
timeout: 10,
});
gather.say('Please enter your SSN number followed by the pound sign.');
res.send(twiml.toString());
});
Here, we’re using Twilio’s <Gather> verb to collect numeric input from the caller. When this TwiML executes, the caller will hear the prompt to enter their SSN number. Twilio will then collect their input.
The <Gather> verb is configured with these attributes:
input: dtmf stands for Dual-Tone Multi-Frequency tones, expecting numeric input from the caller.
action: Specifies the endpoint responsible for handling user input. When the caller finishes entering digits (or the timeout is reached), Twilio will send an HTTP request to this URL, including the caller's input and Twilio's standard request parameters (CallSid, from, to, etc.)
finishOnKey: Allows setting a key (in this case, #) for the caller to submit their input. When Twilio detects #, it stops waiting for additional input and submits it to the action URL (excluding the #).
timeout: Twilio will wait for the caller to press another digit for 10 seconds before sending the data to the action URL. The default timeout is 5 seconds.
Authenticating the caller
Now it's time to implement caller verification.
We'll create /twilio/webhook/authenticate route, which will receive the caller’s SSN number. This route will asynchronously call our previously defined authenticate function while instructing the caller to authenticate via their Swedish BankID app.
For simplicity, we'll store the idToken (authentication result) in a global variable. In a production environment, you would keep user authentication data in a database.
Add the following code to get-call.js:
let idToken;
app.post('/twilio/webhook/authenticate', async (req, res) => {
const userInput = req.body.Digits;
// Format user input so the numbers are spoken individually by Twilio
// https://www.twilio.com/docs/voice/twiml/say#hints-and-advanced-uses
const spokenUserInput = userInput.split('').join(' ');
console.log(Userinput: ', userInput);
const twiml = new VoiceResponse();
// Respond to user while executing CIBA asynchronously
twiml.say(`Your SSN is: ${spokenUserInput}. Please verify it using your bank app`);
// Pause for 10 seconds to keep the call alive
twiml.pause({ length: 10 });
twiml.redirect({ method: 'POST' }, '/twilio/webhook/result);
// Send the TwiML response;
res.type('xml');
res.send(twiml.toString());
// Call CIBA asynchronously
try {
idToken = await authenticate(userInput);
console.log('from idToken await', idToken);
} catch {
console.error('Authentication error', error);
idToken = null;
}
});
Authentication results
The /twilio/webhook/result route is the final step in the user verification flow, where user data can be accessed and used to provide personalized responses.
app.post('/twilio/webhook/result', async (req, res) => {
const twiml = new VoiceResponse();
console.log('idToken: ', idToken);
const checkToken = () => {if (idToken) {
twiml.say('Thank you for verifying your identity. Your account balance is $5000. Goodbye!');
} else {
twiml.say('Something went wrong. Please try again later.');
}
res.type('xml');
res.send(twiml.toString());
};
// Set a delay before checking the token
setTimeout(checkToken, 5000); // // 5 seconds delay
});
Upon successful authentication ( i.e., if idToken is not null), Twilio will inform the caller about their account balance. We’re using a generic account balance statement to keep it simple. In reality, the data in the JWT token includes the caller’s personal information, so you can greet them by name, look up specific details in your database using a unique identifier, etc.
Testing the workflow
Now it’s time to put our work to the test. If everything has been done correctly, you can now dial your Twilio number and input the SSN of your test user on the phone.
Twilio will ask you to authenticate. You can then open your BankID app, respond to the security check message, and go through the authentication process.
Twilio will confirm you’ve been verified, and terminate the call.
Thank you for following along! We hope you’ve enjoyed this article.
If you have any questions, feedback, or use cases you'd like to discuss, please contact us on Slack or by email.
You can also continue learning about Caller authentication on our add-on page.