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:
- Email and password authentication
- 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:
A command in Terminalwire that launches a web browser and sends the user to a URL where they can authenticate.
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.