Module: Everywhere::AuthToken
- Defined in:
- lib/everywhere/auth_token.rb
Overview
The sealed envelopes the native auth flow passes around: the marker cookie that says "this browser session is completing a sign-in for the app", and the handoff token that carries the resulting cookie jar back into the app's web view.
Self-contained (encrypt-then-authenticate, expiry inside the payload) rather than a server-side store: the flow crosses processes — and in a load-balanced deploy, hosts — between the auth browser and the web view, and a shared store would be one more thing to configure. AES-256-GCM with the purpose as associated data, so a marker can never be replayed as a handoff token.
Defined Under Namespace
Classes: MissingSecret
Constant Summary collapse
- CIPHER =
"aes-256-gcm"- NONCE_BYTES =
12- TAG_BYTES =
16
Class Attribute Summary collapse
Class Method Summary collapse
-
.decrypt(token, purpose:, once: false) ⇒ Object
The payload, or nil for anything we can't fully vouch for: tampered, wrong purpose, expired, or (when
once) already spent. - .encrypt(payload, purpose:, ttl:) ⇒ Object
-
.reset! ⇒ Object
Test hook — a new secret means a new key.
Class Attribute Details
.secret ⇒ Object
31 32 33 34 35 36 37 38 |
# File 'lib/everywhere/auth_token.rb', line 31 def secret @secret ||= discover_secret or raise(MissingSecret, <<~MESSAGE) Native sign-in needs a secret to seal its handoff tokens. Rails apps use secret_key_base automatically; elsewhere set SECRET_KEY_BASE (or EVERYWHERE_AUTH_SECRET) in the environment, or assign Everywhere::AuthToken.secret at boot. MESSAGE end |
Class Method Details
.decrypt(token, purpose:, once: false) ⇒ Object
The payload, or nil for anything we can't fully vouch for: tampered,
wrong purpose, expired, or (when once) already spent. Never raises on
bad input — every one of these arrives from outside.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/everywhere/auth_token.rb', line 53 def decrypt(token, purpose:, once: false) raw = Base64.urlsafe_decode64(token.to_s) return nil if raw.bytesize <= NONCE_BYTES + TAG_BYTES decipher = OpenSSL::Cipher.new(CIPHER).decrypt decipher.key = key decipher.iv = raw.byteslice(0, NONCE_BYTES) decipher.auth_tag = raw.byteslice(NONCE_BYTES, TAG_BYTES) decipher.auth_data = purpose.to_s payload = JSON.parse(decipher.update(raw.byteslice(NONCE_BYTES + TAG_BYTES..)) + decipher.final) return nil unless payload.is_a?(Hash) && payload["exp"].to_i > now return nil if once && !claim(token, payload["exp"].to_i) payload rescue ArgumentError, OpenSSL::Cipher::CipherError, JSON::ParserError nil end |
.encrypt(payload, purpose:, ttl:) ⇒ Object
40 41 42 43 44 45 46 47 48 |
# File 'lib/everywhere/auth_token.rb', line 40 def encrypt(payload, purpose:, ttl:) sealed = JSON.generate(payload.merge("exp" => now + ttl)) cipher = OpenSSL::Cipher.new(CIPHER).encrypt cipher.key = key nonce = cipher.random_iv cipher.auth_data = purpose.to_s ciphertext = cipher.update(sealed) + cipher.final Base64.urlsafe_encode64(nonce + cipher.auth_tag + ciphertext, padding: false) end |
.reset! ⇒ Object
Test hook — a new secret means a new key.
73 74 75 76 77 |
# File 'lib/everywhere/auth_token.rb', line 73 def reset! @secret = nil @key = nil @spent = nil end |