Two Factor Authentication

| Python | Node | Ruby |


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.

Copy
Copied
API_KEY='YOUR_API_KEY'
FROM_NUMBER='YOUR_TELNYX_NUMBER'

Note: After pasting the above content, Kindly check and remove any new line added

Note: This file contains a secret key, it should not be committed to source control.

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.

Copy
Copied
DEBUG=True
COUNTRY_CODE='+1'
TOKEN_LENGTH=4

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
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()]

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
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')

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
def get_random_token_hex(num_chars):
    byte_data = secrets.token_hex(math.ceil(num_chars / 2.0))
    return byte_data.upper()[:num_chars]

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
@app.route('/request', methods=['POST'])
def handle_request():
    phone_number = (request.form['phone']
                    .replace('-', '').replace('.', '')
                    .replace('(', '').replace(')', '')
                    .replace(' ', ''))

Note: After pasting the above content, Kindly check and remove any new line added

Then generate a token and add the token/phone number pair to the data store.

Copy
Copied
    generated_token = get_random_token_hex(app.config['TOKEN_LENGTH'])
    TokenStorage.add_token(generated_token, phone_number)

Note: After pasting the above content, Kindly check and remove any new line added

Finally, send an SMS to the device and serve the verification page.

Copy
Copied
    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')

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
@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')

Note: After pasting the above content, Kindly check and remove any new line added

Otherwise, send the user back to the verify form with an error message

Copy
Copied
    else:
        return render_template('verify.html', display_error=True)

Note: After pasting the above content, Kindly check and remove any new line added

Finishing up

At the end of the file, run the server.

Copy
Copied
if __name__ == "__main__":
    app.run()

Note: After pasting the above content, Kindly check and remove any new line added

To start the application, run python otp_demo.py from within the virtualenv.

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.

Copy
Copied
{
    "API_KEY": "YOUR_API_KEY",
    "FROM_NUMBER": "YOUR_TELNYX_NUMBER"
}

Note: After pasting the above content, Kindly check and remove any new line added

Note: This 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.

Copy
Copied
{
    "NODE_ENV": "development",
    "COUNTRY_CODE": "+1",
    "TOKEN_LENGTH": 4 
}

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
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 = {}

Note: After pasting the above content, Kindly check and remove any new line added

Server initialization

Setup a simple Express app that watches the templates directory with Nunjucks, load the config file, and configure the telnyx library.

Copy
Copied
const config = require('./config.json';)
const express = require("express")
const app = express()

app.use(express.urlencoded());
app.set('views', `${__dirname}/templates`);

expressNunjucks(app, {
  watch: true,
  noCache: true,
});

Note: After pasting the above content, Kindly check and remove any new line added

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

Copy
Copied
app.get('/', (_req, res) => {
  res.render('index');
}); 

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
function get_random_token_hex(num_chars) {
  return crypto.randomBytes(num_chars).toString('hex');
}

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
app.post('/request', (req, res) => {
  let phone_number = req.body.phone.replace('/[\-\.\(\)]/g','')

Note: After pasting the above content, Kindly check and remove any new line added

Then generate a token and add the token/phone number pair to the data store.

Copy
Copied
  let generated_token = get_random_token_h(config.TOKEN_LENGTH)
  TokenStorage.add_token(generated_token, phone_number)

Note: After pasting the above content, Kindly check and remove any new line added

Finally, send an SMS to the device and serve the verification page.

Copy
Copied
  telnyx.messages.create({
    "to":   `${phone_number}`,
    "from": `${config.COUNTRY_CODE}${config.FROM_NUMBER}`,
    "text": `Your Token is ${generated_token}`
    })

  res.render('verify.html');
})

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
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')
  }

Note: After pasting the above content, Kindly check and remove any new line added

Otherwise, send the user back to the verify form with an error message

Copy
Copied
  else {
    res.render('verify.html')
  }
})

Note: After pasting the above content, Kindly check and remove any new line added

Finishing up

At the end of the file, run the server.

Copy
Copied
const port = 3000
app.listen(port, () => console.log(`App listening on ${port}!`))

Note: After pasting the above content, Kindly check and remove any new line added

To start the application, run node index.js.

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.

Copy
Copied
API_KEY: 'YOUR_API_KEY'
FROM_NUMBER: 'YOUR_TELNYX_NUMBER'
COUNTRY_CODE: '+1'
TOKEN_LENGTH: 4

Note: After pasting the above content, Kindly check and remove any new line added

Note: This 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.

Copy
Copied
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

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
$config = YAML.safe_load(File.open('config.yml').read)
Telnyx.api_key = $config['YOUR_API_KEY']

get '/' do
  erb :index
end

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
def getRandomTokenHex(numChars)
   return SecureRandom.hex(numChars) 
end

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
post '/request' do
    phoneNumber = params['phone']
                    .gsub('-','').gsub('.','')
                    .gsub('(','').gsub(')','')
                    .gsub(' ','')

Note: After pasting the above content, Kindly check and remove any new line added

Then generate a token and add the token/phone number pair to the data store.

Copy
Copied
    generatedToken = getRandomTokenHex($config["TOKEN_LENGTH"])
    TokenStorage.addToken(generatedToken, phoneNumber)

Note: After pasting the above content, Kindly check and remove any new line added

Finally, send an SMS to the device and serve the verification page.

Copy
Copied
    Telnyx::Message.create(
        from: "#{$config["COUNTRY_CODE"]}#{$config["FROM_NUMBER"]}",
        to: "#{phoneNumber}",
        text:"Your token is #{generatedToken}",
    )
    
    erb :verify

end

Note: After pasting the above content, Kindly check and remove any new line added

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.

Copy
Copied
get '/verify' do 
    token = params['token']

    if TokenStorage.tokenIsValid(token)
        TokenStorage.clearToken(token)
        erb :verify_sucess

Note: After pasting the above content, Kindly check and remove any new line added

Otherwise, send the user back to the verify form with an error message

Copy
Copied
    else
      erb :verify, locals: {
        display_error: True
      }
    end
end

Note: After pasting the above content, Kindly check and remove any new line added

Finishing up

To start the application, run ruby 2fa.rb.