Skip to content

Instantly share code, notes, and snippets.

@janko
Last active December 19, 2023 19:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janko/87f598d670482820b1cf2989fbe04239 to your computer and use it in GitHub Desktop.
Save janko/87f598d670482820b1cf2989fbe04239 to your computer and use it in GitHub Desktop.
Rodauth 2FA example (TOTP & recovery codes) via JWT
require "roda"
require "sequel"
require "rack/test"
require "json"
DB = Sequel.sqlite
DB.create_table :accounts do
primary_key :id
String :status_id, default: 1, null: false
String :email, null: false
String :password_hash
end
DB.create_table :account_otp_keys do
foreign_key :id, :accounts, primary_key: true
String :key, null: false
Integer :num_failures, null: false, default: 0
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
end
DB.create_table :account_recovery_codes do
foreign_key :id, :accounts, type: :Bignum
String :code
primary_key [:id, :code]
end
class RodauthApp < Roda
plugin :rodauth, json: :only do
enable :create_account, :login, :otp, :recovery_codes, :jwt
hmac_secret "secret"
jwt_secret "secret"
account_password_hash_column :password_hash
require_login_confirmation? false
require_password_confirmation? false
auto_add_recovery_codes? true
auto_remove_recovery_codes? true
after_otp_setup { json_response[:recovery_codes] = recovery_codes }
end
route do |r|
r.rodauth
end
end
session = Rack::Test::Session.new(RodauthApp)
session.header "Content-Type", "application/json"
session.header "Accept", "application/json"
# Registration
session.post "/create-account", JSON.generate(login: "user@example.com", password: "secret123")
session.header "Authorization", session.last_response.headers["Authorization"]
# TOTP & recovery codes setup
session.post "/otp-setup", {}
secrets = JSON.parse(session.last_response.body).slice("otp_secret", "otp_raw_secret")
rotp = ROTP::TOTP.new(secrets["otp_secret"])
session.post "/otp-setup", JSON.generate(**secrets, otp: rotp.now, password: "secret123")
# Login
session.post "/logout"
session.post "/login", JSON.generate(login: "user@example.com", password: "secret123")
session.header "Authorization", session.last_response.headers["Authorization"]
# TOTP authentication
DB[:account_otp_keys].update(last_use: Time.now - 60) # add delay since last time a code was used
session.post "/otp-auth", JSON.generate(otp: rotp.now)
JSON.parse(session.last_response.body) #=> { "success" => "You have been multifactor authenticated" }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment