Two factor authentication (2FA) docs
2FA for Python
⏱ 20 minutes build time || Difficulty Level: Intermediate || Github Repo
Configuration file
Create a config.cfg
file in your project directory. Flask will load this at startup. First, use this guide to provision an SMS number and messaging profile, and create an API key. Then add those to the config file.
NoteThis file contains a secret key, it should not be committed to source control.
API_KEY='YOUR_API_KEY'
FROM_NUMBER='YOUR_TELNYX_NUMBER'
We’ll also place Flask in debug mode, assume all numbers are in the U.S., and specify the number of characters we'd like the OTP token to be.
DEBUG=True
COUNTRY_CODE='+1'
TOKEN_LENGTH=4
Token storage
We'll use a class to store tokens in memory for the purposes of this example. In a production environment, a traditional database would be appropriate. Create a class called TokenStorage
with three methods. This class will store uppercase tokens as keys, with details about those tokens as values, and expose check and delete methods.
class TokenStorage():
tokens = {}
@classmethod
def add_token(cls, token, phone_number):
cls.tokens[token] = {
'phone_number': phone_number,
'last_updated': datetime.now(),
'token': token.upper()
}
@classmethod
def token_is_valid(cls, token):
return token.upper() in cls.tokens
@classmethod
def clear_token(cls, token):
del TokenStorage.tokens[token.upper()]
Server initialization
Setup a simple Flask app, load the config file, and configure the telnyx library. We'll also serve an index.html
page, the full source of this is available on GitHub, but it includes a form that collects a phone number for validation.
app = Flask(__name__)
app.config.from_pyfile('config.cfg')
telnyx.api_key = app.config['API_KEY']
@app.route('/')
def serve_index():
return render_template('index.html')
Token generation
We'll start with a simple method, get_random_token_hex
, that generates a random string of hex characters to be used as OTP tokens.
def get_random_token_hex(num_chars):
byte_data = secrets.token_hex(math.ceil(num_chars / 2.0))
return byte_data.upper()[:num_chars]
The token_hex
method accepts a number of bytes, so we need to divide by two and and round up in order to ensure we get enough characters (two characters per byte), and then finally trim by the actual desired length. This allows us to support odd numbered token lengths.
Next, handle the form on the /request
route. First this method normalizes the phone number.
@app.route('/request', methods=['POST'])
def handle_request():
phone_number = (request.form['phone']
.replace('-', '').replace('.', '')
.replace('(', '').replace(')', '')
.replace(' ', ''))
Then generate a token and add the token/phone number pair to the data store.
generated_token = get_random_token_hex(app.config['TOKEN_LENGTH'])
TokenStorage.add_token(generated_token, phone_number)
Finally, send an SMS to the device and serve the verification page.
telnyx.Message.create(
to=app.config['COUNTRY_CODE'] + phone_number,
from_=app.config['FROM_NUMBER'],
text='Your token is ' + generated_token
)
return render_template('verify.html')
Token verification
The verify.html
file includes a form that collects the token and sends it back to the server. If the token is valid, we'll clear it from the datastore and serve the success page.
@app.route('/verify', methods=['POST'])
def handle_verify():
token = request.form['token']
if TokenStorage.token_is_valid(token):
TokenStorage.clear_token(token)
return render_template('verify_success.html')
Otherwise, send the user back to the verify form with an error message
else:
return render_template('verify.html', display_error=True)
Finishing up
At the end of the file, run the server.
if __name__ == "__main__":
app.run()
To start the application, run python otp_demo.py
from within the virtualenv.
2FA for Node
⏱ 20 minutes build time || Difficulty Level: Intermediate || Github Repo
Configuration
Create a config.json
file in your project directory. Express will load this at startup. First, use this guide to provision an SMS number and messaging profile, and create an API key. Then add those to the config file.
{
"API_KEY": "YOUR_API_KEY",
"FROM_NUMBER": "YOUR_TELNYX_NUMBER"
}
NoteThis file contains a secret key, it should not be committed to source control.
We’ll also place Node in debug mode, assume all numbers are in the U.S., and specify the number of characters we'd like the OTP token to be.
{
"NODE_ENV": "development",
"COUNTRY_CODE": "+1",
"TOKEN_LENGTH": 4
}
Token storage
We'll use a class to store tokens in memory for the purposes of this example. In a production environment, a traditional database would be appropriate. Create a class called TokenStorage
with three methods. This class will store uppercase tokens as keys, with details about those tokens as values, and expose check and delete methods.
class TokenStorage{
static add_token(token, phone_number){
this.tokens[token] = {
'phone_number': phone_number,
'last_updated': Date.now(),
'token': token.toUpperCase()
}
}
static token_is_valid(token){
return token in TokenStorage.tokens
}
static clear_token(token){
delete TokenStorage.tokens[token]
}
}
TokenStorage.tokens = {}
Server initialization
Setup a simple Express app that watches the templates directory with Nunjucks
, load the config file, and configure the telnyx library.
import config from './config.json';
import express from "express";
const app = express()
app.use(express.urlencoded());
app.set('views', `${__dirname}/templates`);
expressNunjucks(app, {
watch: true,
noCache: true,
});
Collect user input
Create a simple HTML form, index.html, which collects the phone number for validation. The full HTML source can be found at our Github repo, and we'll serve the root
app.get('/', (_req, res) => {
res.render('index');
});
Token generation
We'll start with a simple method, get_random_token_hex
, that generates a random string of hex characters to be used as OTP tokens.
function get_random_token_hex(num_chars) {
return crypto.randomBytes(num_chars).toString('hex');
}
The randomBytes
method accepts a number of bytes, so we need to divide by two and and round up in order to ensure we get enough characters (two characters per byte),and then finally trim by the actual desired length. This allows us to support odd numbered token lengths.
Next, handle the form on the /request
route. First this method normalizes the phone number.
app.post('/request', (req, res) => {
let phone_number = req.body.phone.replace('/[\-\.\(\)]/g','')
Then generate a token and add the token/phone number pair to the data store.
let generated_token = get_random_token_h(config.TOKEN_LENGTH)
TokenStorage.add_token(generated_token, phone_number)
Finally, send an SMS to the device and serve the verification page.
telnyx.messages.create({
"to": `${phone_number}`,
"from": `${config.COUNTRY_CODE}${config.FROM_NUMBER}`,
"text": `Your Token is ${generated_token}`
})
res.render('verify.html');
})
Token verification
The verify.html
file includes a form that collects the token and sends it back to the server. If the token is valid, we'll clear it from the datastore and serve the success page.
app.post("/verify", (req, res) => {
let token = req.body.token
if (TokenStorage.token_is_valid(token)) {
TokenStorage.clear_token(token)
res.render('verify_success.html')
}
Otherwise, send the user back to the verify form with an error message
else {
res.render('verify.html')
}
})
Finishing up
At the end of the file, run the server.
const port = 3000
app.listen(port, () => console.log(`App listening on ${port}!`))
To start the application, run node index.js
.
2FA for Ruby
⏱ 20 minutes build time || Difficulty Level: Intermediate || Github Repo
Configuration
Create a config.cfg
file in your project directory. Flask will load this at startup. First, use this guide to provision an SMS number and messaging profile, and create an API key. Then add those to the config file.
API_KEY: 'YOUR_API_KEY'
FROM_NUMBER: 'YOUR_TELNYX_NUMBER'
COUNTRY_CODE: '+1'
TOKEN_LENGTH: 4
NoteThis file contains a secret key, it should not be committed to source control.
Token storage
We'll use a class to store tokens in memory for the purposes of this example. In a production environment, a traditional database would be appropriate. Create a class called TokenStorage
with three methods. This class will store uppercase tokens as keys, with details about those tokens as values, and expose check and delete methods.
class TokenStorage
@@tokens = {}
def self.addToken(token, phoneNumber)
@@tokens[token] = {
phone_number: phoneNumber,
last_updated: DateTime.now,
token: token.upcase
}
end
def self.tokenIsValid(token)
return @@tokens.key?(token)
end
def self.clearToken(_token)
@@tokens.delete(token)
end
end
Server initialization
Setup a simple Flask app, load the config file, and configure the telnyx library. We'll also serve an index.html
page, the full source of this is available on GitHub, but it includes a form that collects a phone number for validation.
$config = YAML.safe_load(File.open('config.yml').read)
Telnyx.api_key = $config['YOUR_API_KEY']
get '/' do
erb :index
end
Token generation
We'll start with a simple method, get_random_token_hex
, that generates a random string of hex characters to be used as OTP tokens. We'll use the SecureRandom gem for this, as it comes pre-installed in Ruby.
def getRandomTokenHex(numChars)
return SecureRandom.hex(numChars)
end
The SecureRandom.hex
method accepts a number of bytes, so we need to divide by two and and round up in order to ensure we get enough characters (two characters per byte), and then finally trim by the actual desired length. This allows us to support odd numbered token lengths.
Next, handle the form on the /request
route. First this method normalizes the phone number.
post '/request' do
phoneNumber = params['phone']
.gsub('-','').gsub('.','')
.gsub('(','').gsub(')','')
.gsub(' ','')
Then generate a token and add the token/phone number pair to the data store.
generatedToken = getRandomTokenHex($config["TOKEN_LENGTH"])
TokenStorage.addToken(generatedToken, phoneNumber)
Finally, send an SMS to the device and serve the verification page.
Telnyx::Message.create(
from: "#{$config["COUNTRY_CODE"]}#{$config["FROM_NUMBER"]}",
to: "#{phoneNumber}",
text:"Your token is #{generatedToken}",
)
erb :verify
end
Token verification
The verify.html
file includes a form that collects the token and sends it back to the server. If the token is valid, we'll clear it from the datastore and serve the success page.
get '/verify' do
token = params['token']
if TokenStorage.tokenIsValid(token)
TokenStorage.clearToken(token)
erb :verify_sucess
Otherwise, send the user back to the verify form with an error message
else
erb :verify, locals: {
display_error: True
}
end
end
Finishing up
To start the application, run ruby 2fa.rb
.