class Heroku::Bouncer::Middleware

Constants

DEFAULT_LOGIN_PATH
DecryptedHash

Encapsulates encrypting and decrypting a hash of data. Does not store the key that is passed in.

UnableToFetchUserError

Attributes

login_path[R]

Public Class Methods

new(app, options = {}) click to toggle source
Calls superclass method
# File lib/heroku/bouncer/middleware.rb, line 18
def initialize(app, options = {})
  if options[:disabled]
    @app = app
    @disabled = true
    # super is not called; we're not using sinatra if we're disabled
  else
    super(app)
    @disabled = false
    @cookie_secret = extract_option(options, :secret, SecureRandom.hex(64))
    @allow_if_user = extract_option(options, :allow_if_user, nil)
    @login_path = extract_option(options, :login_path, DEFAULT_LOGIN_PATH)
    @redirect_url = extract_option(options, :redirect_url, 'https://www.heroku.com')

    # backwards-compatibilty for `herokai_only`:
    #  * check email for ending with `@heroku.com`
    #  * The redirect URL can be passed as a string value to `herokai_only`
    herokai_only = extract_deprecated_option("please use `allow_if_user` instead", options, :herokai_only, false)
    if herokai_only
      if herokai_only.is_a?(String) && !options[:redirect_url]
        @redirect_url = herokai_only
      end
      @allow_if_user ||= lambda { |user| user['email'].end_with?("@heroku.com") }
    end

    # backwards-compatibility for allow_if
    allow_if = extract_option(options, :allow_if, false)
    if allow_if
      @allow_if_user ||= lambda { |user| allow_if.call(user['email']) }
    end

    @expose_token = extract_option(options, :expose_token, false)
    @expose_email = extract_option(options, :expose_email, true)
    @expose_user = extract_option(options, :expose_user, true)
    @session_sync_nonce = extract_option(options, :session_sync_nonce, nil)
    @allow_anonymous = extract_option(options, :allow_anonymous, nil)
    @skip = extract_option(options, :skip, false)
  end
end

Public Instance Methods

call(env) click to toggle source
Calls superclass method
# File lib/heroku/bouncer/middleware.rb, line 57
def call(env)
  if @disabled || skip?(env)
    @app.call(env)
  else
    unlock_session_data(env) do
      super(env)
    end
  end
end

Private Instance Methods

anonymous_request_allowed?() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 194
def anonymous_request_allowed?
  auth_request? || (@allow_anonymous && @allow_anonymous.call(request))
end
auth_paths() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 170
def auth_paths
  @auth_paths ||= [
    "/auth/heroku/callback",
    "/auth/heroku",
    "/auth/failure",
    "/auth/sso-logout",
    "/auth/logout",
    DEFAULT_LOGIN_PATH,
    login_path
  ].compact.freeze
end
auth_request?() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 182
def auth_request?
  auth_paths.include?(request.path_info)
end
custom_login_path?() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 203
def custom_login_path?
  login_path != DEFAULT_LOGIN_PATH
end
decrypt_store(env) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 243
def decrypt_store(env)
  env["rack.session"][:bouncer] =
    DecryptedHash.unlock(env["rack.session"][:bouncer], @cookie_secret)
end
destroy_session() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 271
def destroy_session
  store.keys.each { |k| store_delete(k) }
end
encrypt_store(env) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 248
def encrypt_store(env)
  if env["rack.session"].key?(:bouncer)
    env["rack.session"][:bouncer] =
      env["rack.session"][:bouncer].lock(@cookie_secret)
  end
end
enforce_host(scheme, host, port, url) click to toggle source

Prevent open redirect vulnerabilities by setting the current host

# File lib/heroku/bouncer/middleware.rb, line 282
def enforce_host(scheme, host, port, url)
  return_to = URI.parse(url) rescue '/'
  return_to.scheme = scheme
  return_to.host = host
  return_to.port = port unless port == 80
  return_to.to_s
end
expired?() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 198
def expired?
  ts = store_read(:expires_at)
  ts.nil? || Time.now.to_i > ts
end
expose_store() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 275
def expose_store
  store.each_pair do |key, value|
    request.env["bouncer.#{key}"] = value
  end
end
extract_deprecated_option(warning, options, option, default = nil) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 220
def extract_deprecated_option(warning, options, option, default = nil)
  $stderr.puts "[warn] heroku-bouncer: `#{option}` option is deprecated: #{warning}" if options.has_key?(option)
  extract_option(options, option, default)
end
extract_option(options, option, default = nil) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 216
def extract_option(options, option, default = nil)
  options.fetch(option, default)
end
fetch_user(token, retries = 3) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 225
def fetch_user(token, retries = 3)
  response = ::Faraday.new(ENV["HEROKU_API_URL"] || "https://api.heroku.com/").get('/account') do |r|
    r.headers['Accept'] = 'application/vnd.heroku+json; version=3'
    r.headers['Authorization'] = "Bearer #{token}"
  end

  if response.status == 200
    ::Heroku::Bouncer::JsonParser.call(response.body)
  elsif retries > 0
    sleep(0.1)
    fetch_user(token, retries - 1)
  else
    raise UnableToFetchUserError
  end
rescue ::Faraday::ClientError, ::Heroku::Bouncer::JsonParserError
  raise UnableToFetchUserError
end
require_authentication() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 211
def require_authentication
  store_write(:return_to, request.url)
  redirect to(login_path)
end
session_nonce_mismatch?() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 186
def session_nonce_mismatch?
  @session_sync_nonce && (store_read(@session_sync_nonce.to_sym).to_s != session_nonce_cookie.to_s) && !auth_request?
end
skip?(env) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 207
def skip?(env)
  @skip && @skip.call(env)
end
store() click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 255
def store
  session[:bouncer]
end
store_delete(key) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 267
def store_delete(key)
  store.delete(key)
end
store_read(key) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 263
def store_read(key)
  store.fetch(key, nil)
end
store_write(key, value) click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 259
def store_write(key, value)
  store[key] = value
end
unlock_session_data(env) { || ... } click to toggle source
# File lib/heroku/bouncer/middleware.rb, line 163
def unlock_session_data(env, &block)
  decrypt_store(env)
  yield
ensure
  encrypt_store(env)
end