Overview of Ruby/Rails Authorizations

Jason Kuffler
8 min readNov 1, 2021

--

Let’s begin by talking about the often discussed browser cookies. In web/dev terms a cookie is a small amount of data sent from a server to a client (ex: browser) which is placed in browser storage and relayed back to the server each time there’s a request made to server.

In it’s true nature HTTP is stateless. This is because servers are not handling the client’s info for each request. Cookies enable a stateful browser experience as a channel to send more info to a server with each request. Cookies are are stored in a browser for each domain (ex: medium.com) and only that domain’s cookies travel between browser and server.

Hey it’s me, client. Got this cookie for golden globe domain server

There are a variety of uses for cookies. Any time you’ve logged into a domain your browser has stashed a cookie to gain verification of your login throughout that site for the entire session (before logging out). Without this functionality each request would require us to reenter our credentials. Who would use social media if not for cookies? Imagine posting something after logging in, then having to re-login to reply to a comment, check another friend’s post, edit your profile, or add a new contact. Logging in is an example of cookies in use to store session info. They are also handy with persisting our personalized settings to a domain. One area of concern for web users is the way cookies are used for analyzing our behavior (tracking users). All in all — without cookies we’re left with even more problems than blocking pesky ad-tracking; therefore, we make do.

Let’s take the latter example of using cookies for a means of tracking/analyzing user interaction. In addition we consider the pieces of an HTTP request:

HTTP verb: GET, PUT, or POST

Path: medium.com/Jkuffler

Headers: “Content-Type”: “application/json”

Recall that HTTP servers (in most cases) are stateless. Upon receiving, then processing requests, and then returning data, the server loses memory of the request. As such, either the path or headers will pass required info in a request. What other way could their be? Answer: none. When we visit a site and notice in the URL bar a given web address such as the following:

https://national-park-app.herokuapp.com/account

we can anticipate that we’re in the “Account” page of the domain’s site. Prior to diving into web and software development I recall discovering these URL path names to be easily navigated with enough familiarity to a regular web site. It’s rather intuitive after all. Then I can remember starting to notice a certain pattern on larger sites handling more traffic. Maybe another good example would be something like https://www.nhl.com/capitals which, in fact totally works! These examples illustrate our server requests coming to the server in the Path portion of the request. Rails knows to pull the value of something like team_name from the path and store it in params[:team_name]. In the teams controller we could guess their show action to be like so

def show
team = Team.find_by(team_name: params[:team_name])
render json: team
end

If one were to search for a /strangers path on the NHL domain they would be met with their 404 exception handler. Something a vast majority of users have found one way or another, albeit a bad link or (like me) goofing around in the URL bar.

Using the path name to send a request is quite common; however, there are instances in which we would rather our requests mask the information being sent. Any news site with a paywall is likely tracking the remaining free articles available through some other means than the path. It would be possible for users to manipulate the URL to get around the paywall. If the URL explicitly shows the number of free views remaining we could simply increase that number. I did just that when a public domain’s API was limiting daily requests within it’s token. I simply increased that number to my liking to avoid having multiple tokens in my code. The path requests help in easier user navigation, but leaves much to be desired when a server wants to perform any type of tracking of user events/behaviors. Cookies are therefore storing information in the only other option for developers: HTTP headers.

Let’s zoom in on Cookies. What are they?

In layman’s terms: a cookie is served up to a user’s browser with an id equal to a seemingly random set of characters/numbers. The user’s browser will stash the cookie for that domain. Upon subsequent requests to the same domain their server verifies a cookie passing in the request’s headers. When a request is made without a cookie then one is automatically assigned.

From the cookie spec

== Server -> User Agent ==
Set-Cookie: SID=31d4d96e407aad42

== User Agent -> Server ==
Cookie: SID=31d4d96e407aad42

Using the page_views example from earlier a request response cycle can be set up on the server to eventually display a paywall. Here’s the quick example I got learning from Flatiron School. Below shows the cookie being created

== Server -> User Agent ==
Set-Cookie: pageviews_remaining=5

Now we would require the browser to include the cookie in request headers:

== User Agent -> Server ==
Cookie: pageviews_remaining=5

We could likewise write conditional logic to decrement the count accordingly or return a message indicating that a subscription is required to continue reading.

Fact remains that one can still see a cookie in plain text. Chrome’s developer tools displays them under Application > Cookies. Users can delete them at will or edit them with a cookie editor extension. This means the above example is just as flimsy as the URL path. We could still view pages ad infinitum.

Rails Encryption and Signatures

Rails will encrypt and sign cookies by way of the session method:

session[:pageviews_remaining] = 5

The session method serializes the key/value pairs set with session. That is to say they go from being a Ruby object to a big string. Above we’ve set the key within the session method to page_views_remaining.

Done correctly Rails will add a signature to the cookie which avoids user manipulations. Inside config/credentials.yml.enc is your rails server key.

development:
secret_key_base: 2039ufak93239732239yhfk3oh100k477ha71m3ncryp73d

Elsewhere in Rails the signature is generated through encryption. The key is necessary in “decoding” a signature in order to prevent forgery. The signature now created — Rails will append it to the cookie. Now a match is required in order to continue a user session w/ credentials, preferences, and tracking persisting after signed in or while browsing a site. Any tampering to a signature is thus ignored by the server and a new session is assigned.

Cookie Configuration

The way I’ve learned to use Ruby on Rails is primarily as an API builder using the following:

rails new newappname --api

which will exclude built-in code required to work with sessions and cookies. Read on if you are interested in manually adding cookies back into a Rails app.

We need to update the application’s configuration in the config/application.rb file. Below is a quick peek at the required changes (in bold) from Flatiron course material:

class Application < Rails::Application
config.load_defaults 6.1
# This is set in apps generated with the --api flag, and removes session/cookie middleware
config.api_only = true

# Must add these lines!
# Adding back cookies and session middleware
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore


# Use SameSite=Strict for all cookies to help protect against CSRF
config.action_dispatch.cookies_same_site_protection = :strict

Adding in the necessary middleware for cookies and sessions is as simple as those added lines in the config/application.rb file. The last added line addresses our application’s security against cross-site contamination. The SameSite policy for cookies, when set to strict, will only allow the browser to send this site’s cookies in requests made on the same domain.

Next we’ll move over to the top level controller ApplicationController to ensure access to our cookies in the appropriate children controllers. Also from Flatiron course material, the ApplicationController will look like this

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::Cookies
end

This way the concept of inheritance works in our favor by allowing all controllers to work with cookies!

Viewing Cookies in the Browser

The following is a quick walk-thru for Chrome users. Other browsers support cookie viewing similarly :)

  1. Open the Dev-Tools with Ctl+Shift+I
  2. Find the Application Tab
  3. Open Cookies in the menu on the left underneath Storage

Take note: the cookies you find here can be edited. You can change their values directly in the browser. Sessions; however, can not be manipulated. Rails security (signing and encryption) disable the user from editing their hash’s values.

Should look something like this

from a pair project I did https://national-park-app.herokuapp.com/

Authentication

The term authentication in developer terms simply refers to the fact that in some way our app is confirming a user’s identity is who the say they are. A great analogy for web authentication follows the keycard pattern. A hotel will issue you a keycard for their building once you’ve authenticated your registration by showing the required ID or Credit card for the reservation. Once verified you can use their keycard to gain access to your room, their gym, the pool, etc. . . When you’ve left the building only to return later you do not show front desk your ID all over again now that you have the keycard (cookie). Similarly you check your mail by validating your credentials (name and password). Their server will verify your info and serve your browser the cookie. Anywhere else you visit on their domain this cookie will let you load the desired content (calendar, photo albums, etc . . .)

So what’s this look like for us as a Rails developer? The following example does not illustrate use of password protection. It’s very basic to help understand the flow for now.

  1. User hits login/signup form on their browser
  2. They enter their credentials
  3. Form is submitted as a POST to /login on our Rails backend.
  4. The SessionsController’s create action sets a cookie for the browser. The user ID is written into session hash.
  5. Requests after cookie-set will hold user ID “session[:user_id]”

The SessionsController will appear as such as described above:

class SessionsController < ApplicationController
def create
user = User.find_by(username: params[:username])
session[:user_id] = user.id
render json: user
end
end

On the front end our handleSubmit (inside a Login component) will contain the following

function handleSubmit(e) {
e.preventDefault();
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
})
.then((r) => r.json())
.then((user) => onLogin(user));
}

userName will be set in state and the user’s submit action will trigger the new value for userName. The onLogin callback function will save the logged in user’s details. In this example it could be passed down from a parent component which will ideally handle the bulk of rendering state.

In order to persist the front-end logged-in state we do the following. First create a route to retrieve user’s data from the backend using the session’s hash.

get "/me", to: "users#show"

Also a controller action to handle successful and unauthorized requests

class UsersController < ApplicationController
def show
user = User.find_by(id: session[:user_id])
if user
render json: user
else
render json: { error: "Not authorized" }, status: :unauthorized
end
end
end

To log a user in from the browser upon app load return to the top level of your front end React components. Here we’re saying that for each OK response ensure the user is IDed.

function App() {
const [user, setUser] = useState(null);

useEffect(() => {
fetch("/me").then((response) => {
if (response.ok) {
response.json().then((user) => setUser(user));
}
});
}, []);

if (user) {
return <h2>Welcome, {user.username}!</h2>;
} else {
return <Login onLogin={setUser} />;
}
}

Similarly there are some easy steps for logging out a user. First create the route

delete "/logout", to: "sessions#destroy"##SessionsController#destroy action##
def destroy
session.delete :user_id
head :no_content
end
//FRONT END example
function NavBar({ onLogout }) {
function handleLogout() {
fetch("/logout", {
method: "DELETE",
}).then(() => onLogout());
}

return (
<header>
<button onClick={handleLogout}>Logout</button>
</header>
);
}

This stripped-down, simplistic view of authorization sets up for more complex verifications (using a password) and refined functionaliy (authenticating). Authenticating user actions requires more checks and may be discussed in future blog topics as I continue my journey as a new developer!

--

--

Jason Kuffler

Creative. Researcher. Coder. Rad Dad. Sharing easy-to-follow progress covering my tech ed