Delving into Session-Based Authentication

Introduction to Session-Based Authentication

Welcome to this lesson on Session-Based Authentication. In the previous lesson, we delved into API authentication using API keys, focusing on how these keys act as a passcode for gaining access to protected endpoints. Here, we take a step further and explore session-based authentication, a method where the server maintains user session information, providing a stateful experience. Unlike API keys, which are stateless, session-based authentication provides a user-friendly way to manage active sessions, track user interactions, and facilitate access to resources. By the end of this lesson, you will be capable of signing up, logging in, accessing protected resources, and logging out using session-based authentication.

Understanding Session-Based Authentication

Session-based authentication is a process that allows users to stay logged into a system as they interact with different endpoints in an application. Whether you are using a web browser or a client like a Ruby script to interact with a RESTful API, session-based authentication involves maintaining user session information on the server, creating a stateful experience.

When you log in to a RESTful API using your Ruby client, the server starts a "session" for you. This session is like a temporary ID card that validates your identity during your interactions with the API. A critical part of this process involves a "cookie," which is a small piece of data sent from the server and stored by your client.

In the context of a Ruby-based client:

  • Session Creation: After logging in by sending a POST request with your username and password, the server creates a unique session ID, often returned in a cookie.
  • Ongoing Requests: Your Ruby client, with the help of libraries like Net::HTTP, can include this session ID in subsequent requests to the API. This allows the server to recognize the session without needing to re-enter your credentials.
  • Session Termination: When your client logs out by sending a logout request, the server invalidates the session, stopping further requests with the old session ID from succeeding, safeguarding your session integrity.

By using libraries such as Net::HTTP in Ruby, you can manage cookies and session data seamlessly, enabling effective interaction with RESTful API endpoints while maintaining user state securely.

Step 1: Signing Up

In the first step of session-based authentication, you need to create a user account. This action is performed by sending a POST request to the API's signup endpoint. Below is the code example showing how this can be accomplished using Net::HTTP:

require 'net/http'
require 'uri'
require 'json'

# Base URL for the API
base_url = "http://localhost:8000"

# Signup details with a username and password
auth_details = {
  "username" => "testuser",
  "password" => "testpass123"
}

# Creating a URI object for the signup endpoint
uri = URI.parse("#{base_url}/auth/signup")

# Attempt to sign up to the API
begin
  # Create a new HTTP session
  http = Net::HTTP.new(uri.host, uri.port)

  # Create a POST request with the user details
  request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
  request.body = auth_details.to_json

  # Send the request and get the response
  response = http.request(request)

  # Raise an error if the signup request was unsuccessful
  raise "HTTP Error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

  # Print a success message and the server's JSON response if signup is successful
  puts "Signed up successfully!"
  puts JSON.parse(response.body)

rescue => e
  # Print error details if the HTTP request fails
  puts "An error occurred: #{e.message}"
end

In this example, a user account is created by submitting a POST request to the /auth/signup endpoint, along with a username and password. If the request is successful, a confirmation message and the server’s response are displayed; otherwise, error details are provided. Upon successful signup, you would see the following output, confirming that the account has been created:

Signed up successfully!
{"message" => "Signup successful. Please log in to continue."}

Since the API doesn't establish or return a session during signup, you have the option to use Net::HTTP::Post directly. However, using a session from the outset ensures code consistency and prepares you for future steps where session persistence becomes crucial. In scenarios where an API logs you in automatically during signup, maintaining the session guarantees that the session is maintained, allowing immediate access to protected endpoints without an additional login.

Step 2: Logging In

After signing up, the next step is to log in and initiate a session. This involves posting login credentials to an authentication endpoint and leveraging Ruby's HTTP libraries to maintain session state. Here's an example of how this is implemented:

# Attempt to log in to the API
begin
  # Create a URI object for the login endpoint
  uri = URI.parse("#{base_url}/auth/login")

  # Create a new HTTP session
  http = Net::HTTP.new(uri.host, uri.port)

  # Create a POST request with the user credentials
  request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
  request.body = auth_details.to_json

  # Send the request and get the response
  response = http.request(request)

  # Raise an error if the login request was unsuccessful
  raise "HTTP Error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

  # Print a success message and the server's JSON response if login is successful
  puts "Logged in successfully!"
  puts JSON.parse(response.body)

  # Retrieve and print the session ID from cookies
  session_id = response['Set-Cookie']
  puts "\nSession ID: #{session_id}"

rescue => e
  # Print error details if the HTTP request fails
  puts "An error occurred: #{e.message}"
end

In this code snippet, after signing up, you proceed to log in via a POST request to /auth/login, re-using the HTTP session to keep the session active. Successful login messages are shown if the credentials are accepted and the session is active, and the session ID is printed; otherwise, error details are displayed. Successful login results in the following output:

Logged in successfully!
{"message" => "Login successful"}

Session ID: session=eyJhdXRoZW50aWNhdGVkIjp0cnVlLCJ1c2VybmFtZSI6InRlc3R1c2VyMjIyIn0.Z5JFJQ.sF2NX5wOgSF5GVqEmuS2YAawIWM

Step 3: Accessing Protected Endpoints

With an active session established, you can simply use your HTTP session to make requests to protected endpoints of the API. Here's an example of how to achieve this:

# Attempt to access a protected endpoint
begin
  # Create a URI object for the protected resource
  uri = URI.parse("#{base_url}/todos")

  # Create a new HTTP session
  http = Net::HTTP.new(uri.host, uri.port)

  # Create a GET request with the session ID in the headers
  request = Net::HTTP::Get.new(uri.path)
  request['Cookie'] = session_id

  # Send the request and get the response
  response = http.request(request)

  # Raise an error if the GET request was unsuccessful
  raise "HTTP Error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

  # Print a success message and the server's JSON response if access is successful
  puts "Accessed todos successfully!"
  puts JSON.parse(response.body)

rescue => e
  # Print error details if the HTTP request fails
  puts "An error occurred: #{e.message}"
end

Here, a GET request is used to access the /todos endpoint, assuming it's protected and requires an active session. When access is granted, it indicates that your session is valid and active; otherwise, error responses are provided in case of a failure. Accessing the protected resource successfully will produce the following output:

Accessed todos successfully!
[{"description" => "Milk, eggs, bread, and coffee", "done" => false, "id" => 1, "title" => "Buy groceries"}, {"description" => "Check in and catch up", "done" => true, "id" => 2, "title" => "Call mom"}, {"description" => "Summarize Q4 performance metrics", "done" => false, "id" => 3, "title" => "Finish project report"}, {"description" => "30 minutes of cardio", "done" => true, "id" => 4, "title" => "Workout"}]

Step 4: Ending the Session with Logging Out

To end the session and log out by targeting the /auth/logout endpoint, ensuring that the session is cleanly terminated, you can use the following:

# Attempt to log out from the API
begin
  # Create a URI object for the logout endpoint
  uri = URI.parse("#{base_url}/auth/logout")

  # Create a new HTTP session
  http = Net::HTTP.new(uri.host, uri.port)

  # Create a POST request to end the session
  request = Net::HTTP::Post.new(uri.path)
  request['Cookie'] = session_id

  # Send the request and get the response
  response = http.request(request)

  # Raise an error if the logout request was unsuccessful
  raise "HTTP Error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

  # Print a success message and the server's JSON response if logout is successful
  puts "Logged out successfully!"
  puts JSON.parse(response.body)

rescue => e
  # Print error details if the HTTP request fails
  puts "An error occurred: #{e.message}"
end

To log out, a POST request is made to /auth/logout, notifying the server to terminate the session. A successful logout message is displayed upon completion; otherwise, error responses are provided if the request fails. After logging out, you will see the following message as confirmation:

Logged out successfully!
{"message" => "Logout successful"}

Attempting to Access Protected Endpoints After Logout

After ending the session and logging out, any attempt to access protected endpoints without logging back in will fail. This is because the session is no longer active, and the server won't recognize your session ID. Here’s the output you can expect when trying to access a protected endpoint after logging out:

An error occurred: HTTP Error: 401

Summary and Preparation for Practice

In this lesson, we journeyed through the process of session-based authentication, covering signing up, logging in, accessing secure resources, and logging out. By maintaining an active session, you have learned to manage user states effectively and securely — an important aspect of API interaction. As you transition to the practice exercises, these methods will form the backbone of your hands-on experience in securing endpoints. The upcoming practices are designed to reinforce your understanding and mastery of session-based authentication, preparing you for subsequent lessons that explore even more advanced methods like JSON Web Tokens (JWT). Dive into these exercises with confidence, applying what you've learned in a practical, results-oriented way.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal