Last active
December 19, 2023 19:51
-
-
Save janko/87f598d670482820b1cf2989fbe04239 to your computer and use it in GitHub Desktop.
Rodauth 2FA example (TOTP & recovery codes) via JWT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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