Class: Everywhere::AuthHandoff
- Inherits:
-
Object
- Object
- Everywhere::AuthHandoff
- 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"- MARKER_COOKIE =
Set by START, read on the way back out. Ours, not the app's session: it must survive whatever the app does to its own session mid-flow, and it must be droppable from the jar we replay.
"_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
-
.marker(env) ⇒ Object
The start marker riding a request, decrypted, or nil.
-
.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.
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, root: Dir.pwd) ⇒ AuthHandoff
constructor
A new instance of AuthHandoff.
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 |