DIY Authentication Provider

Let's implement OAuth 2.0 for fun, learning, and appreciation of the tech

Vlad Lebedev

Sr. Tech Lead at Mixbook

Experienced Software Engineer
Conscious Incompetent in security

vlad@mixbook.com

Why OAuth?

Depends only on HTTP transport

Designed to fit well into distributed systems

Every respectable service implements it

OAuth Roles

Client Application

Authorization Provider

Resource Server

Resource Owner

Decentralized Architecture

Hi there, microservices :)

Client
Application

Authorization
Provider

Resource
Server

Resource
Owner

Everyone does it

$ history | grep "token"

OAuth is an ancient technique applied over HTTP

* IMHO

Bearer Token from XIII century

Paiza – a tablet carried by Mongol officials and envoys to signify certain privileges and authority

Kublai Khan grants access token to Marco Polo

Potayto, potahto

Authorization != Authentication

Authorization

Grant/deny access for a resource to a user

OAuth is an authorization protocol

Authentication

Verify the identity of a user

OpenID Connect is an authentication protocol based on OAuth

It's a stack!

Getting Started

Resource Server

Server hosting the protected resources. This is the API you want to access.

The spec

RSpec.describe 'Resource Server', type: :request do
  specify 'resource is protected'

  context 'when bearer token is supplied' do
    specify 'resource is available'

    context 'when token expires' do
      specify 'resource is not available'
    end

    context "when scope doesn't match" do
      specify 'resource is not available'
    end
  end
end
            

The spec

RSpec.describe 'Resource Server', type: :request do
  let(:headers) { Hash.new }

  before { get '/housing', headers: headers }

  it 'is protected' do
    expect(response).to have_http_status(:bad_request)
  end

  context "when bearer token is supplied" do
    let(:token) { create(:access_token, scope: 'housing') }
    let(:headers) { { 'Authorization' => "Bearer #{token.value}" } }

    specify 'resource is available' do
      expect(response).to have_http_status(:ok)
    end

    context 'when token expires' do
      let(:token) do
        create(:access_token, expires_at: 1.second.ago, scope: 'housing')
      end

      specify 'resource is not available' do
        expect(response).to have_http_status(:forbidden)
      end
    end

    context "when scope doesn't match" do
      let(:token) { create(:access_token, scope: 'food') }

      specify 'resource is not available' do
        expect(response).to have_http_status(:forbidden)
      end
    end
  end
end
            

Primitive Implementation

class EmpireController < ApplicationController
  before_action :authorize!

  def housing
    head :ok
  end

  private

  def authorize!
    token_header = request.headers["Authorization"]
    unless token_header.present?
      head :bad_request
      return
    end

    token_value = token_header.delete_prefix("Bearer ")
    token = AccessToken.find_by(value: token_value)
    head :forbidden unless token&.usable?(['housing'])
  end
end
           

Primitive Implementation

class AccessToken < ApplicationRecord
  def usable?(claimed_scopes)
    scope_list.intersection(claimed_scopes) == claimed_scopes && !expired?
  end

  def expired?
    expires_at < Time.current
  end

  def scope_list
    scope.split(' ')
  end
end
           

Resource Owner

a.k.a The User

The Spec

RSpec.describe 'Resource Owner', type: :system do
  specify do
    visit '/'
    click_on 'Sign Up'
    fill_in 'Email', with: 'kublai_khan@mongolia.mn'
    fill_in 'Password', with: 'Tengri'
    fill_in 'Password confirmation', with: 'Tengri'
    click_on 'Sign up'
    expect(page).to have_text('You have signed up successfully.')

    click_on 'Sign Out'
    expect(page).to have_text('Signed out successfully.')

    click_on 'Sign In'
    fill_in 'Email', with: 'kublai_khan@mongolia.mn'
    fill_in 'Password', with: 'Tengri'
    click_on 'Log in'
    expect(page).to  have_text('Signed in successfully.')
  end
end

            

Just use Devise

              
$ bundle add devise
$ bundle exec rails g devise:install
$ bundle exec rails g devise User
              
            
  • Password Authentication
  • Password Recovery
  • Activity Tracking
  • Email Confirmation
  • Password Enumeration Locking

Client Application

Application requesting access to a protected resource on behalf of the Resource Owner

Client Key is public

Client Secret should only be known by the auth provider and the client app. It can be transported only via Back-End channel i.e. server to server

Houston, we have a problem

Mobile apps and some SPAs don't have a Back-End channel

PKCE

Proof Key for Code Exchange

Extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks

Authorization Code Flow With PKCE

     +---------------------------------------------------+
     |                 Authz Provider                    |
     +----^--+------^-------+----------------------------+
          |  |Authz |       |                 
          |  |Code  |/token |Token            
/authorize|  |      |       |Grant            
          |  |      |       |                               
          |  |      |       |                          
     +----+--v------+-------+----------------------------+
     |    |         |       |                            |
     |   Marco Polo Karavan v                            |
     |  mpolo://oauth/success                            |
     |                                                   |
     +---------------------------------------------------+
                      Mobile Device
            

Authorization Code Flow With PKCE

     +---------------------------------------------------+
     |                 Authz Provider                    |
     +----^--+------^-------+--------^-------+-----------+
          |  |Authz |       |        |       |
          |  |Code  |/token |Token   |/token |
/authorize|  |      |       |Grant   |       | (PKCE)
          |  |      |       |        |       X code_verifier
          |  |      |       |        |       | mismatch
     +----+--v------+-------+--------+-------+-----------+
     |    |         |       |        |       v           |
     |   Marco Polo Karavan v       Gang of Robbers      |
     |  mpolo://oauth/success    mpolo://oauth/success   |
     |                                                   |
     +---------------------------------------------------+
                      Mobile Device
            

Authorization Code Flow with PKCE

In Code

              
            

Implementation

            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            

Authentication

  • Add openid, profile, and email scopes to /authorize request
  • Include id_token to /token response
  • Provide /userinfo endpoint and/or use JWT with OIDC claims for id_token

Implementation

              
                
              
            

Implementation

              
                
              
            

Resources & Further Reading

Coming Soon: Mixbook's 2023 Tech Experts Speaker Series

← Sign up for updates for future events here

Vă mulţumesc pentru atenţie

Спасибо за внимание

Hvala na pažnji

Thank you for your attention