Add AuthKit Login to Rails

Add hosted login to Rails and inspect, refresh, and revoke a session cookie.

Introduction

30 Second Summary

A login can look finished while the invisible state behind it is already out of date. Those hidden details surface when a private page stays open or logout behaves differently away from your laptop.

In this project, you will add hosted sign in to a small Ruby on Rails app with WorkOS AuthKit. You will trace the session beneath that login across its full lifecycle.

What You'll Build

You will open a private page that shows the session claims your server decoded from a sealed browser cookie.

By the end of this project, you'll have:

  • A protected page that redirects signed-out visitors to hosted sign in. Signed-in visitors can open it.
  • Readable session claims displayed inside your app. You can compare them with the opaque cookie stored by your browser.
  • A self-renewing session that stays active after expiry during a refresh. A sign-out check that restores the redirect to hosted sign in.
  • Secret Mission: An optional challenge to push your session debugging skills further.

Are there any prerequisites?

You need a local Ruby on Rails setup. You also need a free WorkOS account. A browser with a cookie inspector completes the setup.

Before We Start

Your authentication flow depends on a reliable local foundation. Ruby runs the application. Rails provides the web framework that serves each page.

In this step, you will install the required versions of Ruby and Rails. You will also prepare Bundler and the browser tools you will use to inspect the session cookie.

In this step, get ready to:
  • Install Ruby 3.2 or newer.
  • Prepare Bundler and Rails 8.1.0 or newer.
  • Confirm your browser developer tools can inspect cookie storage.
Verify or install Ruby

Rails 8.1 requires Ruby 3.2 or newer. A version manager keeps this project away from the older Ruby release that may come with your operating system.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Terminal on macOS or PowerShell on Windows.
  • Press Enter to open the terminal.
  • Check your current Ruby version by running this command:
ruby --version

What Does This Check Show?

The command prints the Ruby release available in this terminal. The first two numbers must be 3.2 or higher for this project.

✔️ I see Ruby 3.2 or higher

Ruby meets the Rails requirement. Continue below to confirm the terminal is using this release consistently.

ⓧ I see an older version

Your terminal is finding an outdated Ruby installation. Use the official Rails installation path for your operating system to install a current Ruby release.

macOS

The macOS setup uses Mise to manage Ruby versions. The first command opens a system installation dialog.

  • Start the Xcode Command Line Tools installation by running this command:
xcode-select --install
What Does This Command Do?

The command opens the macOS installer for the compiler tools Ruby needs. Complete the installer before continuing.

The dependency setup can take a few minutes while macOS downloads packages. A password prompt may appear because the installer is changing software on your computer.

  • Install Homebrew dependencies by running these commands:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
brew install openssl@3 libyaml gmp rust
What Does This Setup Do?

Homebrew installs the libraries used to compile Ruby. Updating your shell path makes the Homebrew commands available in new terminal sessions.

  • Install Mise and Ruby by running these commands:
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate)"' >> ~/.zshrc
source ~/.zshrc
mise use -g ruby@3
What Does Mise Do?

Mise installs a current Ruby 3 release. It also updates your shell so that the managed Ruby takes priority over the older system copy.

Windows

The official Rails guide uses Windows Subsystem for Linux with Ubuntu. This gives Rails a Linux environment similar to the servers that commonly host it.

  • Install Ubuntu through Windows Subsystem for Linux by running this command in PowerShell:
wsl --install --distribution Ubuntu-24.04
What Does This Command Do?

The command installs Ubuntu inside Windows. Windows may ask you to approve the change or restart your computer.

  • Restart Windows if the installer requests it.
  • Press the Windows key to open your search bar.
  • Type Ubuntu into the search bar.
  • Press Enter to open Ubuntu.
  • Create the local Ubuntu username requested during the first launch.
  • Create the local Ubuntu password requested during the first launch.

The password is stored inside your local Ubuntu environment. Keep it private because later system commands may ask for it.

Ruby compilation can take a few minutes after the dependencies are installed. The terminal may stay busy without printing new lines during parts of the process.

  • Install the Ruby dependencies and Mise by running these commands in Ubuntu:
sudo apt update
sudo apt install build-essential rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
mise use -g ruby@3
What Does This Setup Do?

Ubuntu installs the libraries required to compile Ruby. Mise then installs a current Ruby 3 release for your user account.

ⓧ Command not found

Ruby is not available in this terminal yet. Install it through the official Rails setup for your operating system.

macOS

The macOS setup uses Mise to install Ruby without replacing protected system files. The first command opens a system installation dialog.

  • Start the Xcode Command Line Tools installation by running this command:
xcode-select --install
What Does This Command Do?

The command opens the macOS installer for the compiler tools Ruby needs. Complete the installer before continuing.

The dependency setup can take a few minutes while macOS downloads packages. A password prompt may appear because the installer is changing software on your computer.

  • Install Homebrew dependencies by running these commands:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
brew install openssl@3 libyaml gmp rust
What Does This Setup Do?

Homebrew installs the libraries used to compile Ruby. Updating your shell path makes the Homebrew commands available in new terminal sessions.

  • Install Mise and Ruby by running these commands:
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate)"' >> ~/.zshrc
source ~/.zshrc
mise use -g ruby@3
What Does Mise Do?

Mise installs a current Ruby 3 release. It also activates that release whenever you start a new shell.

Windows

The official Rails guide uses Windows Subsystem for Linux with Ubuntu. This keeps the Rails development environment close to a production Linux server.

  • Install Ubuntu through Windows Subsystem for Linux by running this command in PowerShell:
wsl --install --distribution Ubuntu-24.04
What Does This Command Do?

The command installs Ubuntu inside Windows. Windows may ask you to approve the change or restart your computer.

  • Restart Windows if the installer requests it.
  • Press the Windows key to open your search bar.
  • Type Ubuntu into the search bar.
  • Press Enter to open Ubuntu.
  • Create the local Ubuntu username requested during the first launch.
  • Create the local Ubuntu password requested during the first launch.

The password protects your local Ubuntu account. Store it somewhere safe because later system commands may request it.

Ruby compilation can take a few minutes after the dependencies are installed. Quiet periods in the terminal are normal during this process.

  • Install the Ruby dependencies and Mise by running these commands in Ubuntu:
sudo apt update
sudo apt install build-essential rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
mise use -g ruby@3
What Does This Setup Do?

Ubuntu installs the libraries required to compile Ruby. Mise then installs a current Ruby 3 release for your user account.

  • Confirm the active Ruby release by running this command again:
ruby --version

What Should You See?

The output begins with ruby followed by a version number. That version must be 3.2 or higher.

Good progress. Your terminal now has the Ruby runtime that the authentication app needs.

Still Seeing the Old Ruby Version?

Close the terminal after the installation completes. Open a fresh terminal so the updated shell configuration loads.

On Windows, run the remaining project commands inside Ubuntu. PowerShell may still point to a different Ruby installation.

Ask for help with the version shown in your terminal.

Prepare Bundler and Rails

Ruby applications package reusable code as gems. Bundler keeps the exact gems for one application consistent across different computers.

  • Check whether Bundler is available by running this command:
bundle --version

What Does This Check Show?

The command prints the Bundler version available to Ruby. Modern Ruby distributions usually include Bundler already.

✔️ I see a Bundler version

Bundler is ready to manage the gems in your Rails application.

ⓧ Command not found

Your Ruby installation does not currently expose Bundler. Install the Bundler gem before continuing.

  • Install Bundler by running this command:
gem install bundler

What Does This Command Do?

RubyGems downloads Bundler into the active Ruby installation. The new bundle command becomes available in the same environment.

  • Confirm Bundler is available by running this command:
bundle --version

What Should You See?

You should see a line beginning with Bundler version. This confirms the dependency manager is connected to your active Ruby installation.

Rails is distributed as a Ruby gem. This project uses Rails 8.1.0 or newer so the framework matches the current Rails guide.

  • Check your current Rails version by running this command:
rails --version

What Does This Check Show?

The command prints the Rails release available in this terminal. The version must be 8.1.0 or higher.

✔️ I see Rails 8.1.0 or higher

Rails meets the project requirement. Continue below for one final version check.

ⓧ I see an older version

A newer Rails release can be installed alongside the older gem. The active rails command then uses the latest installed release.

The installation can take a few minutes while RubyGems downloads Rails and its dependencies. Several gem names may scroll through the terminal.

  • Install the latest Rails release by running this command:
gem install rails

What Does This Command Do?

RubyGems installs Rails with the framework libraries it requires. The command makes Rails available from your terminal.

ⓧ Command not found

Rails is not installed in the active Ruby environment. Add the Rails gem to that environment now.

The installation can take a few minutes while RubyGems downloads Rails and its dependencies. Continuous package output means the installation is still working.

  • Install Rails by running this command:
gem install rails

What Does This Command Do?

RubyGems installs Rails with the framework libraries it requires. The new rails command becomes available in the active Ruby environment.

  • Confirm the installed Rails release by running this command:
rails --version

What Should You See?

The output begins with Rails followed by a version number. That version must be 8.1.0 or higher.

Rails Command Still Missing?

Restart the terminal after installation so it reloads the executable path. On Windows, confirm that both the installation and the version check happened inside Ubuntu.

Ask for help diagnosing the active Ruby gem path.

Prepare your browser tools

A successful sign in only shows the visible part of authentication. Browser developer tools let you inspect the cookie stored underneath that flow.

Choose the tab for the browser you plan to use. Both paths end with a developer tools panel that can inspect storage.

Chrome

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Google Chrome into the search bar.
  • Press Enter to open Chrome.
  • Press Cmd+Option+I on macOS or Ctrl+Shift+I on Windows to open developer tools.
  • Select the Application panel.
  • Expand Storage in the sidebar.
  • Select Cookies to confirm the cookie inspector is available.

You should see a cookie table or an empty storage view. This is where you will compare the browser cookie with the session claims Rails reads later.

Safari

  • Press Cmd+Space to open Spotlight.
  • Type Safari into Spotlight.
  • Press Enter to open Safari.
  • Choose Safari from the menu bar.
  • Select Settings.
  • Select Advanced.
  • Enable Show features for web developers.
  • Press Cmd+Option+I to open Web Inspector.

You should see Web Inspector attached to the browser window. Its storage tools will expose the session cookie once your Rails app creates one.

Before the final check, what three version lines do you expect the terminal to print?

  • Verify Ruby, Bundler, and Rails together by running these commands:
ruby --version
bundle --version
rails --version

What Does This Final Check Prove?

  • The first line confirms Ruby 3.2 or newer is active.
  • The second line confirms Bundler can manage the application's gems.
  • The third line confirms Rails 8.1.0 or newer is ready.

That is the environment sorted. Your terminal can now run Rails while your browser can expose the cookie data behind the login flow.

Your tools are ready. Next up, you will create the Rails app and prove its public and private pages are reachable before authentication changes their behavior.

Create the Public and Private Pages

Hosted sign-in eventually decides who reaches your private page. First, your Rails app needs a plain baseline where both URLs work without authentication.

That baseline separates routing mistakes from access-control mistakes. In this step, you will scaffold a working app with two reachable pages.

In this step, get ready to:
  • Create the rails-session-lab Rails application.
  • Generate placeholders for the public page and private page.
  • Wire the public and private routes.
Create the Rails application

The Rails application generator creates the folders needed for your code. It also installs the app's bundled dependencies.

The first scaffold can take a few minutes while Bundler resolves the dependencies. A busy terminal during this command is expected.

  • Create rails-session-lab on your Desktop by running these commands:
# Put the project in a predictable place.
cd ~/Desktop
rails new rails-session-lab

What does this command create?

Rails builds the application inside a new rails-session-lab folder. The generated structure includes dedicated locations for application code and configuration.

  • Confirm the terminal prompt returns after the generator finishes.
  • Open your Desktop in Finder (macOS) or File Explorer (Windows).
  • Confirm the rails-session-lab folder appears on your Desktop.

You should see folders such as app and config inside rails-session-lab. You should also see a Gemfile.

Did the app generator stop?

Read the last lines in the terminal for the dependency that failed. Confirm your internet connection before running the app generator again.

If the folder exists after a failed attempt, remove that incomplete folder before retrying the command.

Help me diagnose why my Rails application generator failed.

Generate the two page endpoints

A browser request moves through three Rails pieces. An HTTP route chooses an action inside a controller.

That action renders a view as the browser response. Rails can generate the controller actions plus their matching view files for you.

  • Move into the Rails app and generate the page files by running these commands:
# Generate two page actions without adding temporary routes.
cd ~/Desktop/rails-session-lab
bin/rails generate controller Pages home private --skip-routes

What did Rails generate?

The generator creates a controller with the home action. It also creates the private action.

Each action gets a matching view template under app/views/pages. The route flag leaves URL design in your hands.

  • Select rails-session-lab from your Desktop in your code editor's folder picker.
  • Expand app/views/pages in the file sidebar.
  • Confirm home.html.erb appears in that folder.
  • Confirm private.html.erb appears in that folder.

Both page templates now exist. Rails still needs exact URL rules before your browser can reach them at / and /private.

Missing one of the view files?

Confirm your terminal was inside rails-session-lab when you ran the generator. Check the generator output for the files it created.

Help me find the missing Rails controller or view file.

The routing file maps each URL to its matching controller action. The root rule makes the home action answer requests to the shortest app URL.

  • Select config/routes.rb in your code editor's file sidebar.
  • Select all existing content in config/routes.rb.
  • Replace the selected content with this routing configuration:
Rails.application.routes.draw do
  # Show the public page at the app's root URL.
  root "pages#home"

  # Keep this page open until authentication is added.
  get "/private", to: "pages#private"
end

What does this routing code do?

  • The root rule sends requests for / to the home action.
  • The get rule sends requests for /private to the private action.
  • Both actions remain open because no authentication guard exists in this step.
  • Save config/routes.rb.
  • Verify the routing table by running this command:
bin/rails routes

How do you read the route list?

The route list shows the request method beside each URL pattern. The final column identifies the controller action that handles the request.

You should find one GET row for / ending in pages#home. You should find another GET row for /private ending in pages#private.

Are the two routes missing?

Confirm you saved config/routes.rb before checking the route list. Check that both route lines sit inside the Rails.application.routes.draw do block.

Help me fix my missing Rails routes.

✔️ Awesome, I've got everything!

Your routing file now maps the public home page and the open private-page placeholder.

ⓧ I'd like to double check the full code

Rails.application.routes.draw do
  # Show the public page at the app's root URL.
  root "pages#home"

  # Keep this page open until authentication is added.
  get "/private", to: "pages#private"
end
Run both pages in the browser

The local development server turns your routes into browser responses. This check proves the page structure works before sign-in changes the request flow.

The first server boot can take several seconds while Rails loads the application. Keep the terminal open after the startup messages appear.

  • Start the local development server by running this command:
bin/rails server

What does the server command do?

Rails starts the application at http://localhost:3000. The server keeps that terminal busy while it listens for browser requests.

  • Leave the server process running in that terminal.
  • Switch back to your browser.
  • Navigate to http://localhost:3000.

You should see the generated page for the home action. The Rails welcome screen has been replaced by your public route.

Before you check the second URL, do you think the app can tell whether you are signed in yet?

  • Navigate to http://localhost:3000/private.

You will see the generated page for the private action without signing in. That open state is the baseline this project needs.

Great, both routes now answer browser requests. You have also exposed the access-control gap that the next steps will close.

Can the browser reach the pages?

Confirm the server terminal is still running. Check that your browser URL starts with http://localhost:3000.

If one page fails while the other works, compare the route list with the two lines in config/routes.rb.

Help me diagnose why my local Rails page is not loading.

Your Rails app now has the open baseline you need. Next, you will connect WorkOS AuthKit and complete the first hosted sign-in.

Connect WorkOS AuthKit

Your Rails app can already serve both routes. The browser still has no authenticated session.

In this step, you will connect WorkOS AuthKit to the app. You will complete one hosted sign-in that returns a sealed session cookie to localhost.

In this step, get ready to:
  • Create a WorkOS account for the hosted sign-in flow.
  • Use the AuthKit installer to connect Rails to WorkOS.
  • Complete a hosted sign-in that creates a browser session.
Create your WorkOS account

The WorkOS Dashboard holds the application settings used by hosted sign-in. Your Rails app connects to the same WorkOS environment through local credentials.

  • Visit the WorkOS Dashboard in your browser.
  • Complete the account sign-up flow with your email address.
  • Finish any email verification requested by WorkOS.

You will use this account when the installer opens its browser authentication flow.

Run the AuthKit installer

The installer detects the Rails project before adding the WorkOS gem. It also creates the authentication routes needed for the hosted flow.

Keep generated credentials private

The installer stores credentials in local environment configuration. Your Rails source files stay free of secrets.

  • Keep your local environment configuration out of screenshots.
  • Keep your local environment configuration out of version control.
  • Switch back to the terminal running your local Rails server.
  • Stop the server by pressing Ctrl+C.

The installer opens a browser for WorkOS authentication.

The integration takes about two minutes while it updates the app. A short pause during validation is expected.

  • Connect AuthKit to the Rails project by running this command:
npx workos@latest integrate

What does the installer change?

  • It detects Rails before installing the workos gem.
  • It adds the routes needed to start sign-in and handle the callback.
  • It stores WORKOS_CLIENT_ID, WORKOS_API_KEY, and WORKOS_COOKIE_PASSWORD in local environment configuration.
  • It updates the WorkOS application settings before validating the integration.
  • Approve the package installation prompt if it appears.
  • Complete WorkOS authentication in the browser window that opens.
  • Return to the terminal after authentication finishes.
  • Wait for the integration validation to finish.

You should see confirmation that AuthKit is ready. This proves the Rails integration passed the installer checks.

Installer does not finish?

  • If npx is unavailable, install Node.js 20 or newer from the official Node.js download page.
  • If browser authentication stalls, finish account verification in the WorkOS tab.
  • If validation fails, run the installer again after fixing the issue named in the terminal.
  • Help me troubleshoot the WorkOS AuthKit installer for my Rails app.

The installer handles the initial dashboard setup. The Redirects page lets you confirm the exact localhost destinations used by this project.

  • Return to the WorkOS Dashboard.
  • Select Applications in the dashboard navigation.
  • Open the application selected by the installer.
  • Select the Redirects tab.
  • Confirm the Redirect URI is http://localhost:3000/callback.
  • Set the Initiate login URL to http://localhost:3000/login.
  • Set the Sign-out URI to http://localhost:3000.
  • Save your redirect changes from the page.

Why do these URLs matter?

The callback URL receives the authorization result after hosted sign-in. The login URL gives AuthKit a route that can begin a fresh sign-in flow.

The sign-out URL controls where WorkOS sends the browser after ending a session. These localhost values keep every redirect inside your development app.

Complete the hosted sign-in

The callback exchanges the WorkOS authorization result for a sealed session. Rails stores that sealed value in the browser cookie created by the integration.

The server keeps this terminal busy while it runs. You can stop it later with Ctrl+C.

  • Switch back to the terminal from the installer.
  • Restart the Rails app with its new AuthKit integration by running this command:
bin/rails server

Why restart Rails?

The previous Rails process started before the AuthKit gem existed. Restarting loads the generated routes with the local environment configuration.

Before you start the flow, consider this question: where will WorkOS send your browser after authentication.

  • Open http://localhost:3000/login in your browser.
  • Complete the hosted sign-in form with your email address.
  • Use the hosted sign-up option if this environment has no app user yet.
  • Complete email verification if WorkOS requests it.

You should return to http://localhost:3000 after authentication. That return confirms the hosted sign-in and callback completed.

  • Open your browser developer tools.
  • Select the cookie storage for http://localhost:3000.
  • Find the cookie named wos_session.

You should see the wos_session cookie with an opaque value. Keep that value private because it represents your signed-in session.

Hosted sign-in does not return?

  • Confirm the dashboard Redirect URI matches http://localhost:3000/callback exactly.
  • Confirm the Rails server restarted after the installer completed.
  • Confirm the terminal still shows the Rails server running.
  • Help me diagnose why hosted sign-in does not return to my Rails app.

You have completed the full hosted authentication round trip. Your browser now holds the sealed session that the next steps will protect and inspect.

AuthKit is connected to your Rails app. Next, you will guard the private page and test both sides of the access check.

Protect the Private Page

Your signed-in browser now carries a valid AuthKit session. The /private route still accepts browsers with no session.

A protected page needs Rails to check authentication before the controller action runs. In this step, you will attach the existing WorkOS guard only to /private.

In this step, get ready to:
  • Protect the private route with an authentication callback.
  • Confirm a signed-out browser reaches hosted sign-in.
  • Confirm your signed-in browser can still open the private page.
Add the authentication guard

Rails controller callbacks can run before a page action. The AuthKit integration already provides require_authentication for this check.

  • Select app/controllers/application_controller.rb in your editor's file sidebar.
  • Add the scoped guard inside the ApplicationController class by copying this code:
# Check AuthKit only when the browser requests the private page
before_action :require_authentication, if: -> { request.path == "/private" }

How does this guard work?

  • Rails runs before_action before it starts the requested controller action.
  • The request.path condition limits the check to /private.
  • The require_authentication method checks the existing sealed session.
  • A failed check redirects the browser before the private page can render.
  • Save app/controllers/application_controller.rb.
Test signed-out access

A private browsing window uses a separate session cookie store. Your normal browser keeps its existing session.

  • Keep the signed-in browser window from earlier available for the final check.
  • Create a private browsing window from your browser's menu.

Before you load the route, do you expect the private page to render or send you to sign-in?

  • Enter http://localhost:3000/private in the private window's address bar.

You will leave the private page and reach hosted WorkOS sign-in. This proves the guard rejected a browser without a valid session.

  • Enter http://localhost:3000 in the same private window's address bar.

You will see the public home page. The path condition has kept / available without authentication.

Still seeing the private page?

  • Confirm that you saved app/controllers/application_controller.rb.
  • Confirm that the callback line sits inside the ApplicationController class.
  • Check that the path in the callback is exactly /private.

Help me find why my signed-out browser can still open the protected Rails route.

Verify signed-in access

The private window proved that a missing session triggers the guard. Your normal browser still holds the valid AuthKit session from earlier.

Before you return to that browser, do you expect the same route to redirect again?

  • Return to your normal signed-in browser window.
  • Enter http://localhost:3000/private in the address bar.

You will stay on /private and see the private-page content. That access boundary now responds correctly to both session states.

Redirected even though you signed in?

  • Confirm that you returned to the original browser window from the completed sign-in flow.
  • Complete hosted sign-in again if that browser no longer has a valid AuthKit session.
  • Check the Rails server output for a session-loading failure after requesting /private.

Help me diagnose why my valid AuthKit session redirects away from the protected Rails page.

Your private page now has a working session boundary. Next, you will inspect the claims that AuthKit reads from the sealed cookie.

Inspect the Session Claims

Your protected /private route already proves that WorkOS AuthKit accepts a valid session. The next question is what the server learned from it.

Rails can expose the session claims that support the signed-in request. Browser developer tools show the sealed cookie that carried them.

In this step, get ready to:
  • Render the server-readable session claims on the private page.
  • Inspect the AuthKit session cookie in browser developer tools.
  • Compare the readable claims with the sealed cookie value.
Render the server-side claims

The browser sends a sealed session cookie with the request to /private.

WorkOS AuthKit unseals that cookie on the server. The private page can render the claims produced by the authentication guard.

  • Switch back to the editor you used earlier for rails-session-lab.
  • Use project search to find the text currently rendered at /private.
  • Add a preformatted development output for the session claims already read by the authentication guard.
  • Save the private-page template.

Before you refresh, which session details do you think the server can display?

  • Return to the signed-in browser tab at http://localhost:3000/private.
  • Refresh the private page.

You should see a readable collection of session fields on the protected page. This confirms that the authentication guard supplied server-readable claims for the request.

Claims missing from the page?

  • Confirm that you edited the existing template rendered by /private.
  • Refresh the private page from the browser session that completed the hosted sign-in.

Ask for help with the specific page output if the claims remain blank: help me diagnose why my protected Rails page is not rendering the AuthKit session claims.

Inspect the sealed browser cookie

The seal uses encryption to turn session material into an opaque browser value. The server uses the configured cookie password to open it.

Keep the session cookie private

A live session cookie can act as a temporary credential. A short visible fragment gives you the comparison without exposing the complete value.

  • Return to the signed-in browser tab at http://localhost:3000/private.
  • Open the developer tools from your browser's page inspection menu.
  • Open the storage view that lists cookies for the current site.
  • Select the cookie storage for http://localhost:3000.
  • Select the AuthKit session cookie created by your completed sign-in.
  • Inspect the cookie value without copying it outside developer tools.

You should see a long opaque value with no readable claim names. This is the sealed session held by the browser.

What is inside the seal?

The sealed session contains the access token. It also contains the refresh token.

User data shares the same encrypted bundle. The server opens that bundle before it authenticates the request.

Cannot find the session cookie?

  • Refresh http://localhost:3000/private while developer tools remain open.
  • Complete the hosted sign-in again if the private route redirects you away.

Ask for help if the cookie storage remains empty: help me locate the AuthKit session cookie for my local Rails app.

Compare both views

The private page shows the server's interpretation of the session. The cookie inspector shows the browser-held seal.

Before you compare them, do you expect the raw cookie to reveal any field names shown by the server?

  • Compare the readable claims on /private with the opaque value in the cookie inspector.

You should find readable fields on the private page. The browser cookie remains an opaque string.

What does the comparison prove?

The cookie's opacity protects the session material from being read directly in the browser. The readable page proves that Rails successfully unsealed the session on the server.

  • Place the private page beside the open cookie inspector.
  • Narrow the cookie table until only a short fragment of the value remains visible.
  • Capture both views with your operating system's screenshot tool.
  • Save the screenshot for your project record.

You have made the session boundary visible. Your screenshot now shows the server-readable claims beside a safely truncated version of the browser-held seal.

You can now see how one session looks on both sides of the request. Next, you will shorten its lifetime to watch AuthKit renew it after expiry.

Renew an Expired Session

Your protected Rails page can trust a short-lived access token only until that token expires. Without a refresh path, the next request sends a valid user through hosted sign-in again.

In this step, you will shorten the WorkOS AuthKit access token lifetime. You will extend the existing before_action guard so an expired token can renew the sealed session.

In this step, get ready to:
  • Shorten the access token duration for the session experiment.
  • Add session renewal to the existing authentication guard.
  • Wait past the expiry before confirming that the private page stays open.
Shorten the access token duration

An access token proves that the current request belongs to a signed-in user. A short duration lets you reach the expiry during this experiment.

  • Return to the WorkOS dashboard from earlier.
  • Select Applications from the dashboard navigation.
  • Select the application connected to rails-session-lab.
  • Open the Sessions tab.
  • Set Access token duration to the shortest duration available.
  • Confirm that Maximum session length exceeds the access token duration.
  • Confirm that Inactivity timeout exceeds the access token duration.
  • Record the selected duration here: your short access token duration.
  • Save the session settings with the page's save control.

Which lifetime are you changing?

Access token duration controls when the app must validate the session through a refresh. Maximum session length sets the hard limit for the whole sign-in session.

Keeping the maximum session length longer leaves the refresh token valid after the access token expires. That gap creates the renewal window you are testing.

The saved session settings now apply to newly issued access tokens.

Deleting this local cookie only resets the experiment session. Your WorkOS account stays intact.

  • In the browser developer tools from earlier, select the wos_session cookie row.
  • Delete the selected cookie using the inspector's delete control.
  • Refresh http://localhost:3000/private.
  • Complete the hosted sign-in flow when AuthKit redirects you.

You will return to /private with the server-read claims visible. Developer tools will show a newly issued wos_session cookie.

Still seeing the previous session?

Confirm that you deleted the wos_session cookie before refreshing the private page. A token issued before the dashboard change can retain its previous expiry.

Check that you changed Access token duration for the same WorkOS application used by your local credentials.

Ask for help if the new session still uses the earlier duration: help me check why my WorkOS access token duration is not applying to a new Rails session.

Add the refresh path

A refresh token can exchange an expired access token for fresh credentials. The existing authentication guard is the right place to trigger that recovery before the protected action runs.

  • In the editor from earlier, switch back to app/controllers/concerns/authentication.rb.
  • Locate the require_authentication method.
  • Replace the complete method with this version:
def require_authentication
  # Verify the sealed session before the protected action runs.
  workos_session.authenticate => { authenticated:, reason:, user: }

  if authenticated
    @current_user = user
    return
  end

  # A missing cookie has no refresh token to exchange.
  return redirect_to(login_path) if reason == "NO_SESSION_COOKIE_PROVIDED"

  # Give other authentication failures one chance to refresh.
  attempt_refresh
end

What does this guard do?

  • The first branch stores the authenticated user for the private page.
  • The missing-cookie branch keeps signed-out visitors on the existing hosted sign-in path.
  • The final branch hands an expired token to attempt_refresh.
  • Save app/controllers/concerns/authentication.rb.
  • Refresh http://localhost:3000/private while the new token remains active.

You should remain on the private page. The server-read session claims should still render.

Private page stopped loading?

Check that attempt_refresh uses the same spelling in the method call. The next code chunk defines that method.

Confirm that every end from the replacement method is present.

Ask for help with the guard structure: help me check the Ruby syntax in my require_authentication method.

The fallback now needs to rotate the credentials. It must also write the returned sealed session into the browser cookie.

  • Add this method directly below require_authentication inside the Authentication concern:
def attempt_refresh
  # Exchange the refresh token for a new sealed session.
  workos_session.refresh => { authenticated:, sealed_session: }

  return redirect_to(login_path) unless authenticated

  # Persist the rotated credentials in the browser.
  cookies["wos_session"] = {
    value: sealed_session,
    httponly: true,
    secure: Rails.env.production?,
    same_site: :lax
  }

  # Restart the request with the cookie written in this response.
  redirect_to request.url
rescue StandardError => e
  # Clear an unusable session before returning to sign-in.
  Rails.logger.warn("AuthKit refresh failed: #{e.message}")
  cookies.delete("wos_session")
  redirect_to login_path
end

What does this refresh method do?

  • The refresh call uses the refresh token inside the sealed session to request fresh credentials.
  • A successful result replaces the browser's wos_session cookie with the new sealed value.
  • The redirect restarts the request so the browser sends the new cookie back to Rails.
  • The rescue path clears an unusable cookie before returning the visitor to hosted sign-in.
  • Save app/controllers/concerns/authentication.rb.
  • Refresh http://localhost:3000/private once more while the access token remains active.

You should still see the private page with the server-read claims. This confirms that the updated guard accepts an active session.

Seeing a Rails error?

Check that attempt_refresh sits above the final end for the Authentication module.

Confirm that the cookie name is exactly wos_session in both the session loader and refresh method.

Ask for help with the refresh code: help me debug my WorkOS session refresh method in Rails.

Use these tabs to confirm the completed concern before testing the expiry.

✔️ Awesome, I've got everything!

Great. Double-check that you saved app/controllers/concerns/authentication.rb.

ⓧ I'd like to double check the full code

Compare your authentication concern with this completed version.

module Authentication
  extend ActiveSupport::Concern

  included do
    helper_method :current_user
  end

  private

  def workos_session
    @workos_session ||= WORKOS.session_manager.load(
      seal_data: cookies["wos_session"],
      cookie_password: ENV.fetch("WORKOS_COOKIE_PASSWORD")
    )
  end

  def current_user
    @current_user
  end

  def require_authentication
    # Verify the sealed session before the protected action runs.
    workos_session.authenticate => { authenticated:, reason:, user: }

    if authenticated
      @current_user = user
      return
    end

    # A missing cookie has no refresh token to exchange.
    return redirect_to(login_path) if reason == "NO_SESSION_COOKIE_PROVIDED"

    # Give other authentication failures one chance to refresh.
    attempt_refresh
  end

  def attempt_refresh
    # Exchange the refresh token for a new sealed session.
    workos_session.refresh => { authenticated:, sealed_session: }

    return redirect_to(login_path) unless authenticated

    # Persist the rotated credentials in the browser.
    cookies["wos_session"] = {
      value: sealed_session,
      httponly: true,
      secure: Rails.env.production?,
      same_site: :lax
    }

    # Restart the request with the cookie written in this response.
    redirect_to request.url
  rescue StandardError => e
    # Clear an unusable session before returning to sign-in.
    Rails.logger.warn("AuthKit refresh failed: #{e.message}")
    cookies.delete("wos_session")
    redirect_to login_path
  end
end
Watch the expired session renew

The newly issued access token uses the shortened duration. Once that token expires, the next request must recover through the refresh token.

Expect this pause to last a little longer than your short access token duration. The idle time is part of the experiment.

  • Leave http://localhost:3000/private idle until the recorded access token duration has passed.

Before you refresh, do you expect to remain on the private page or visit hosted sign-in again?

  • Refresh http://localhost:3000/private after the access token has expired.

You should remain on /private while the server-read claims render again. AuthKit should not show the hosted sign-in page.

  • Compare the current wos_session value with the raw cookie shown in your saved comparison screenshot.

You should see a different sealed cookie value. That change shows that the refresh rotated the session credentials.

Keep the cookie private

The sealed cookie contains sensitive session material. Keep the saved comparison screenshot private because the cookie can be used to resume the session while it remains valid.

That is the full renewal loop working. Your protected page now survives an expired access token by rotating the sealed session.

Next up, you will sign out deliberately and confirm that the same private route sends you back through authentication.

Sign Out and Verify the Redirect

Your refreshed session proved that the Rails app can keep a valid sign-in alive after expiry. A complete authentication flow also needs a reliable way to end that session.

This final step closes the WorkOS AuthKit session through the app's sign-out flow. The cookie inspector reveals the browser state before the protected route supplies server-side proof.

In this step, get ready to:
  • Confirm the callback, sign-in redirect, and logout redirect settings for local development.
  • End the browser session through the app's sign-out flow.
  • Request the protected page to verify the hosted sign-in redirect.
Document the local return paths

Hosted authentication depends on approved destinations for callback, sign-in, and sign-out transitions. These entries keep the browser returning to your app after each hosted flow.

  • Switch back to the WorkOS dashboard tab from the earlier AuthKit setup.
  • Return to the local-development redirect configuration for rails-session-lab.
  • Confirm the callback setting allows http://localhost:3000.
  • Confirm the sign-in redirect setting allows http://localhost:3000.

You now have the callback destination and sign-in return path confirmed for your local app.

  • Set the logout redirect setting to allow http://localhost:3000.
  • Save the redirect configuration if you changed it.

You have pinned down the dashboard detail that often causes logout failures after deployment.

Why do three redirect settings matter?

  • The callback setting allows WorkOS to return the authentication result to your Rails app.
  • The sign-in redirect setting controls where the browser can go after a successful sign-in.
  • The logout redirect setting controls where the browser can go after the hosted session ends.
Inspect the sign-out result

Sign out should end the session that your previous refresh kept alive. The session cookie inspector gives you a browser-side check before the authentication guard gives final proof.

  • Switch back to the browser tab showing /private.
  • Confirm the refreshed session claims are still visible on the private page.

Your claims confirm that you are starting with the renewed session from the previous experiment.

  • Keep the browser developer tools open with the cookie list from earlier visible.
  • Select the app's sign-out control from the signed-in page.
  • Refresh the cookie list for http://localhost:3000.

You may see the AuthKit session cookie disappear immediately. A remaining value needs the protected-page check below to prove invalidation.

What Does Cleared or Invalidated Mean?

A cleared cookie disappears from browser storage. An invalidated cookie may remain listed.

The server no longer accepts an invalidated cookie as proof of a signed-in session. The protected route gives you the decisive test.

Test the signed-out guard

The authentication guard decides whether the browser still has a valid session. This request tests the same protected route that displayed your claims while signed in.

Before you test, consider whether /private will render the session claims or send you to hosted sign-in.

  • Enter http://localhost:3000/private in the browser address bar.
  • Press Enter to request the protected page.

You'll leave the private page and arrive at hosted WorkOS sign-in. Your session claims will no longer render.

You closed the session loop. The guard now sends a signed-out request back to hosted sign-in.

Still Seeing the Private Page?

  • Type http://localhost:3000/private directly into the address bar to make a fresh request.
  • Repeat the app's sign-out flow from the signed-in page.
  • Switch back to the WorkOS dashboard tab from earlier.
  • Confirm the logout redirect setting allows http://localhost:3000.

Help me diagnose why my Rails private page still loads after WorkOS sign-out.

Secret mission

Audit Logout Against Browser History

Test whether browser history can bring protected session claims back after logout. You will refresh the restored private page to prove that a fresh request still requires authentication.

Clean Up Your Resources

Clean Up Your Resources

Your local Rails server does not create an infrastructure bill. Decide whether to keep the app available, pause its process, or delete its local files.

Resources you used:

  • The local Rails server process at http://localhost:3000.
  • The rails-session-lab project folder containing your Rails source code.
  • The local WorkOS AuthKit environment credentials stored inside rails-session-lab.

Keep everything running

No action needed. Choose this if you are still testing session renewal or extending the authentication flow.

  • Leave the Rails server process running.
  • Keep the rails-session-lab folder in its current location.
  • Keep the saved session inspection screenshot with your project notes.
  • Leave the WorkOS dashboard settings configured for http://localhost:3000.

Pause - I'll come back to this later

Shut down the Rails server to free up local memory. Your app files remain available for your next session.

  • Switch back to the terminal running the Rails server from earlier.
  • Stop the running process with your terminal's interrupt shortcut.
  • Leave the rails-session-lab folder in place.
  • Keep the saved session inspection screenshot with your project notes.
  • Return to the rails-session-lab folder when you are ready to continue.
  • Restart the Rails server using the same launch workflow from the project steps.

Delete - I don't want to use this again

Deleting this folder is permanent. The commands remove only the local app files.

  • Move the saved session inspection screenshot outside rails-session-lab if you stored it there.
  • Switch back to the terminal running the Rails server from earlier.
  • Stop the running process with your terminal's interrupt shortcut.
  • On Windows, move above rails-session-lab before deleting the folder by running these commands:
cd ..
rmdir /s /q rails-session-lab

What Do These Commands Do?

  • The first line moves the terminal to the folder above rails-session-lab.
  • This keeps the terminal outside the folder being removed.
  • The second line deletes the entire local app folder.
  • On macOS or Linux, move above rails-session-lab before deleting the folder by running these commands:
cd ..
rm -rf rails-session-lab

What Do These Commands Do?

  • The first line moves the terminal to the folder above rails-session-lab.
  • This keeps the terminal outside the folder being removed.
  • The second line deletes the entire local app folder.
  • Return to the parent folder shown by your terminal in Finder on macOS or File Explorer on Windows.
  • Confirm that rails-session-lab no longer appears.

The local Rails app is now gone. Its stored environment credentials are gone too.

Your WorkOS account remains available. Its dashboard settings remain unchanged.

Folder Still Present?

  • Confirm that the Rails server is stopped before retrying the delete command.
  • Close any editor window that is still using a file inside rails-session-lab.
  • Help me remove the local rails-session-lab folder safely.

Nice Work!

Nice Work!

Excellent work! Your Rails app now uses WorkOS AuthKit to protect /private. You traced the session from its server-read claims through renewal and sign-out.

You've learned how to:

  • Add hosted authentication to a Rails app. Protect /private while keeping / public.
  • Compare server-rendered session claims with the raw sealed cookie in your browser. This comparison shows why the cookie cannot be read as plain JSON.
  • Verify automatic session renewal through the controller before_action after the shortened expiry. Confirm sign-out removes access to the protected page.
  • Secret Mission: Complete the optional challenge to push your skills further.

Ready to quiz yourself?