Class: Everywhere::AuthHandoff

Inherits:
Object
  • Object
show all
Defined in:
lib/everywhere/auth_handoff.rb

Overview

Third-party sign-in for the native shells, as Rack middleware.

Google refuses OAuth in an embedded web view outright, and Apple and GitHub are only marginally better there (no password manager, no already-signed-in session, 2FA that fights the keyboard). So the shell doesn't use its web view: it hands provider paths to an ASWebAuthenticationSession — the system's own browser, sharing Safari's cookies — and this middleware bridges the two jars, because the session cookie the flow ends with lands in the browser's jar and the app's web view can never read it.

1. shell opens  /everywhere/auth/start?to=/auth/github   (auth browser)
 → marker cookie, redirect to the app's own provider path
2. the app's normal OAuth runs — OmniAuth, or whatever it uses — and
 redirects somewhere signed in
3. this middleware catches that redirect, seals the resulting cookie jar
 into a short-lived token, and redirects to <scheme>://auth?token=…,
 which is the callback ASWebAuthenticationSession is waiting for
4. the shell visits /everywhere/auth/handoff?token=… in its web view,
 which replays the jar there and lands on the app's post-auth page

The app's auth is untouched: nothing here knows a provider from a hole in the ground, only which paths auth.oauth_paths says start a flow.

Rails apps get this from Everywhere::Engine (at the top of the middleware stack, above any OmniAuth-family middleware). Sinatra/Hanami add it in config.ru — first, for the same reason — alongside the config endpoint:

use Everywhere::AuthHandoff
use Everywhere::MobileConfigEndpoint

Apps on OmniAuth 2 also need Everywhere::OmniAuth.protect! — see everywhere/omniauth.rb for why its POST-only request phase and this flow have to be introduced to each other.

Constant Summary collapse

START =
"/everywhere/auth/start"
NATIVE =
"/everywhere/auth/native"
HANDOFF =
"/everywhere/auth/handoff"
"_everywhere_auth"
MARKER_PURPOSE =
"everywhere/auth-marker"
HANDOFF_PURPOSE =
"everywhere/auth-handoff"
MARKER_TTL =

How long a started flow stays interceptable. Long enough for a password manager, a 2FA code and a slow provider; short enough that an abandoned flow can't divert an ordinary browser session later.

900

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, root: Dir.pwd) ⇒ AuthHandoff

Returns a new instance of AuthHandoff.



59
60
61
62
63
# File 'lib/everywhere/auth_handoff.rb', line 59

def initialize(app, root: Dir.pwd)
  @app = app
  @root = root
  @mutex = Mutex.new
end

Class Method Details

.marker(env) ⇒ Object

The start marker riding a request, decrypted, or nil. Class-level because the OmniAuth request validator (everywhere/omniauth.rb) asks the same question of the same cookie.



341
342
343
344
345
# File 'lib/everywhere/auth_handoff.rb', line 341

def marker(env)
  raw = request_jar(env)[MARKER_COOKIE] or return nil

  AuthToken.decrypt(Rack::Utils.unescape(raw), purpose: MARKER_PURPOSE)
end

.request_jar(env) ⇒ Object

The request's cookies, values left exactly as they arrived — re-escaping a decoded value would corrupt any cookie the app didn't escape the same way. First occurrence wins on a duplicate name, matching Rack's parser (browsers send the most specific cookie first), so the jar sealed here is the jar the app actually read.



328
329
330
331
332
333
334
335
336
# File 'lib/everywhere/auth_handoff.rb', line 328

def request_jar(env)
  env["HTTP_COOKIE"].to_s.split(/;\s*/).each_with_object({}) do |pair, jar|
    name, value = pair.split("=", 2)
    next if name.nil? || value.nil?

    name = name.strip
    jar[name] = value unless jar.key?(name)
  end
end

Instance Method Details

#call(env) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/everywhere/auth_handoff.rb', line 65

def call(env)
  config = config_for(env)
  return @app.call(env) unless config&.oauth?

  case env["PATH_INFO"]
  when START then return start(env, config)
  when NATIVE then return native_page(env)
  when HANDOFF then return handoff(env, config)
  end

  divert(env, config) || finish(env, config)
end