Authentication

Authentication

Implement login for terminal applications

At some point you’ll want people to login to your terminal application so they can securely access their data from the command-line. Terminalwire can be configured an infinite number of ways to authenticate users, but for the sake of brevity we’ll focus on the most common methods:

  1. Email and password authentication
  2. Web browser authentication

But first, let’s briefly look at sessions to understand how the client maintains it’s login state.

Email and password authentication

The simplest way to authenticate users is to use email and password authentication. This method is secure and easy to implement.

# ./app/terminals/main_terminal.rb
desc "login", "Login to your account"
def login
  print "Email: "
  email = gets.chomp

  print "Password: "
  password = getpass

  # Replace this with your own authentication logic; this is an example
  # of how you might do this with Devise.
  user = User.find_for_authentication(email: email)
  if user && user.valid_password?(password)
    self.current_user = user
    puts "Successfully logged in as #{current_user.email}."
  else
    puts "Could not find a user with that email and password."
  end
end

The important method is getpass, which reads the users input without showing it on screen preventing users from exposing their passwords to people who may be looking at their screen.

The gets method is used to get the persons username, which is shown on screen.

Finally, these two inputs are passed into the same authentication logic you’d use in your Rails controller to authenticate users.

Web browser authentication

Authentication isn’t always as simple as email and password. For more complex authorization workflows, you may want to use a web browser to authenticate users. This is useful if you want to use OAuth, SAML, or other authentication methods.

Web-based authentication requires two main components:

  1. A command in Terminalwire that launches a web browser and sends the user to a URL where they can authenticate.

  2. A controller in your Rails app that handles the authentication and sends the user back to the terminal application.

Terminalwire authentication command

The Terminalwire command will launch a web browser and send the user to a URL where they can authenticate. The URL will contain a token that the Rails app can use to authenticate the user. This token should be a secure nonce that expires after a short period of time.

# ./app/terminals/main_terminal.rb
class MainTerminal < ApplicationTerminal
  desc "login", "Login to application"
  def login
    # Create a nonce
    nonce = SecureRandom.hex
    # Encrypt it with Rails secret key
    verifier = ActiveSupport::MessageVerifier.new(Rails.application.secret_key_base)
    # Generate a token that expires in 10 minutes
    token = verifier.generate(nonce, expires_in: 10.minutes)
    # Generate a URL that the user can visit to authenticate
    url = new_terminal_session_url(token:)

    puts "Opening #{url} in your browser..."
    # How sign it with Rails and gem it into the URL...
    client.browser.launch(new_terminal_session_url(token:))

    # Wait for the user to authenticate. This is a blocking call that waits
    # fot the user to successfully authenticate in the browser.
    authorization = ActiveExchange::Channel.new(name: "auth:#{nonce}")

    if user_id = JSON.parse(authorization.read).fetch("user_id")
      self.current_user = User.find(user_id)
      puts "Welcome #{current_user.email}"
    else
      puts "Well that didn't work"
    end
  end
end

The new_terminal_session_url(token:) URL launches the users browser to a page in your Rails application that asks the user to “Approve” or “Deny” the authorization request.

Terminal authentication controller

The webpage launched by the Terminalwire command will be handled by a controller in your Rails app. The example controller provided below shows how to handle the authentication request and send the user back to the terminal application.

# ./app/controllers/terminal/sessions_controller.rb
class Terminal::SessionsController < ApplicationController
  # If the user is not logged in, this will force them to login before they
  # can authorize the terminal app.
  before_action :authorize_user

  # Enabled inline rendering of the views below.
  layout false
  include Superview::Actions
  View = DeveloperView

  # Display a form asking the user to Approve or Deny the terminal app.
  class New < View
    def title = "Authorize command line app"
    def subtitle = "The terminal app is requesting access to your account. Do you approve?"

    def view_template
      form action: url_for(action: :create), method: :post, class: "flex flex-row gap-4" do
        input type: "hidden", value: helpers.params.fetch("token"), name: "token"
        input type: "submit", class: "btn btn-primary", value: "Approve"
        input type: "cancel", class: "btn btn-error", value: "Deny"
      end
    end
  end

  # Display a success message after the user has approved the terminal app.
  class Show < View
    def title = "Successfully authorized command line app"
    def subtitle = "You may now close this window and continue using the command-line interface."

    def view_template
    end
  end

  # The New view submits a form to this action. If the user approves the terminal authorization
  # it will send a message to the terminal app with the user's ID to the nonce they're listening on.
  def create
    verifier = ActiveSupport::MessageVerifier.new(Rails.application.secret_key_base)
    nonce = verifier.verified(helpers.params[:token])

    message = JSON.generate(user_id: current_user.id)
    ActiveExchange::Channel.new(name: "auth:#{nonce}").broadcast(message)
    redirect_to terminal_session_url
  end
end

The authorize_user method is a before_action that checks if the user is logged in before they can authorize the terminal app. If the user is not logged in, they will be redirected to the login page.

This implementation may work differently in your application depending on the authentication method or libraries you’re using. For example, if you’re using OAuth, you may need to redirect the user to the OAuth provider’s login page instead of your own login page.