Skip to content

Instantly share code, notes, and snippets.

@mikedao
Created December 13, 2022 18:09
Show Gist options
  • Save mikedao/dff4545787de6cd2c6ec12d3ef5d03f8 to your computer and use it in GitHub Desktop.
Save mikedao/dff4545787de6cd2c6ec12d3ef5d03f8 to your computer and use it in GitHub Desktop.

#Authorization

So yesterday we learned about authentication and today we are going to talk about its partner, authorization.

  • How would you define authentication?
  • With that definition in mind, how would you define authorization?

I like to think of authentication as being able to prove that you are who you say you are, but authorization is being allowed where you are allowed.

Example, I can show up to the White House with my photo ID and passport and I can prove to the people there that I am who I am, but they’re not going to let me into the Oval Office. Probably the best I can swing is going to be a public restroom and the gift shop.

And even the gift shop is going to be hit or miss.

So how do we do implement this in rails?

This can be potentially troublesome. The app can verify that I am who I am, but the only mechanism that we currently have for that is to enter our username and password. But clearly, we don’t have to enter in our username and password every time we navigate to a new page, or do something in any web app - that would be incredibly irritating even in the modern day when we have password managers and we can store credentials locally.

So we need some way, preferably a simple way in order to persist some data when I am interacting with a web app.

Hopefully a secure way. It has to be secure, because what if I were just to tell a web application that I am the user with the ID number of one.

User one is generally the first user created on an app and therefore most likely going to be given admin privileges. I would absolutely love it if I could just roll up to Amazon and say, hey I’m user number one, now send me a big screen tv, and don’t charge for it. Actually, add a few million dollars in credit to my account too.

But we can’t do that. Why?

SEGUE

  • What do you know about cookies? Other than the fact that every single website now asks you to accept them thanks to Europe?

Using Cookies

It works just like a hash.

cookies[:user_id] = "12"

This is all we have to do to set a user id in our cookie. It is important to note that everything is going to be a string. We can store other things in there, but we have to convert it to JSON first.

We can do more interesting things with cookies, such as set an expiry, and you can learn more on how to do that here: [https://api.rubyonrails.org/v5.2.1/classes/ActionDispatch/Cookies.html]

And you can delete the keys in a cookie like so: cookies.delete :user_id

This is useful for when someone to log out.

Lets Use Cookies

We can set a cookie and look at its contents in chrome.

#app/controllers/welcome_controller.rb

def index unless cookies[:greeting] cookies[:greeting] = "Hello there" end end

#app/views/welcome/index.html.erb

<%= cookies[:greeting] %>

When we load the page we get Hello there but we can go into our inspector and change things.

Not great. But cookies gives us some more tools.

cookies.signed[:greeting] = "Hello there"

And now the view <%= cookies.signed[:greeting] %>

Well, the “signed” text is still readable with a little extra work.

At the time of writing this lesson, a signed greeting of ‘Howdy!’ was signed like the following cookie value:

Ikhvd2R5ISI=--d12208b183689c5f30379f30d149b481d23f1cd2

If we grab the first portion of the string:

Ikhvd2R5ISI=

We can use “base64 decoding” to turn this back into plaintext.

Visit https://www.base64decode.org/ and paste that text above, and it should turn that string into "Howdy!" which is our string. The remaining portion after the -- which included d12208b... is the “signature” that our Rails application added to the value which verifies that the data has not been tampered with.

If we use that same site to base64 encode “Howdy” without the exclamation point, we would see it generate this string:

Ikhvd2R5Ig==

If we alter our browser cookie so our value is this instead, but keeping the same signature portion:

Ikhvd2R5Ig==--d12208b183689c5f30379f30d149b481d23f1cd2

If we reload the page, our cookie greeting is now blank!

This, again, is Rails protecting itself from using tampered data.

But malicious software can still detect that these cookies are “signed” and that a portion of it is still base64 encoded, and still be able to read that data!

So what we can do is encrypt the stuff we are putting in.

cookies.encrypted[:greeting] = 'Hello there!'

<%= cookies.encrypted[:greeting] %>

Back to Auth

User Roles

At a high level, we sometimes want to have different “kinds” of users in our application like an “admin” user versus a “regular” user, maybe a “management” user.

  • We need to add a role to our user model
  • We need to have controller code based on the user role
    • We need more routes
    • We need to use namespacing

Implementation

We generally would make the user role an integer value so we’re not storing a string over and over, and we can tell Ruby to use a lookup table called an “enum” (short for enumerable) to convert that number to a string later.

How we order these values doesn’t really matter, but it’s important to note that we generally only add to the END of our enumerable list. If we add something in the middle of the list, we might accidentally change other roles, and that can get really confusing.

Rails also has some neat “magic” about using these enum strings to build validation routines that we’ll see in a moment.

Add a new role field

Make a migration to add a role field for a user, which is an integer field:

rails g migration AddRoleToUsers role:integer

The migration should look something like this. Be sure to set the default to 0, which we’ll set to be a “default” user, like a regular user with no special access.

class AddRoleToUsers < ActiveRecord::Migration[5.2] def change add_column :users, :role, :integer, default: 0 end end

Run rails db: migrate to apply this change.

In our User model, we need to specify our list of enumerable strings for our Roles:

class User < ApplicationRecord has_secure_password

enum role: %w(default manager admin) end

What this does is that it gives us some useful extra methods:

  • default?
  • manager?
  • admin?

Logging in

When a user logs in now, we should get a different dashboard based on role. user = User.find_by_email(params[:email].downcase)

if user && user.authenticate(params[:password]) session[:user_id] = user.id if user.admin? redirect_to admin_dashboard_path elsif user.manager? redirect_to manager_dashboard_path elsif user.default? redirect_to user_dashboard_path end else flash[:error] = "Your credentials are bad and you should feel bad" render :new end

Namespacing

#config/routes.rb namespace :admin get '/dashboard', to: 'dashboard#index' end

Now, inside our /app/controllers/ path we need to add a new folder called admin, and create a dashboard controller in there:

#app/controllers/admin/dashboard_controller.rb class Admin::DashboardController < ApplicationController def index end end

Go ahead and make a view here too.

But lets test. #spec/features/admin/login_spec.rb describe "Admin login" do describe "happy path" do it "I can log in as an admin and get to my dashboard" do admin = User.create(name: "alice", email: "[email protected]", password: "super_secret_passw0rd", role: 2)

  visit login_path
  fill_in :email, with: admin.email
  fill_in :password, with: admin.password
  click_button 'log in'

  expect(current_path).to eq(admin_dashboard_path)
end

end end

You will also want to add a test that a user cant go where they arent supposed to.

Pro Tip: use a stub to test sad path.

class Admin::DashboardController < ApplicationController before_action :require_admin

def index end

private def require_admin render file: "/public/404" unless current_admin? end end

Current admin, what?

We can make another helper method in our primary application_controller:

#app/controllers/application_controller.rb def current_admin? current_user && current_user.admin? end

Current user, what?

#app/controllers/application_controller.rb class ApplicationController < ActionController::Base helper_method :current_user

def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end

def current_admin? current_user && current_user.admin? end end

Resources: Authentication with the current user workflow:

[https://backend.turing.edu/module2/lessons/authentication]

sample sad path test

describe "as default user" do it 'does not allow default user to see admin dashboard index' do user = User.create(name: "fern brady" email: "[email protected]", password: "password", role: 0)

  allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user)

  visit "/admin/dashboard"

  expect(page).to have_content("The page you were looking for doesn't exist.")
end

end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment