Module: Hecks::Adapters::GoogleAuthentication
- Defined in:
- lib/hecks/adapters/driven/google_authentication.rb
Overview
GOOGLE'S OWN OIDC HANDSHAKE — the authentication port's one real
implementation today, moved here from being hand-rolled per-app
(an embryonaut_console google_auth.rb used to do exactly this;
any hecks-based app gets Google sign-in for free now, the
same "one adapter, reusable everywhere" value every other adapter
in this directory already has).
oauth2 does ONLY the authorization-code exchange (no Rack
middleware, no Omniauth strategy indirection) ; google-id-token
does ONLY ID-token verification (signature checked against
Google's real, rotating JWKS — real, maintained code, never
hand-rolled here). Neither library decides what a verified
(issuer, subject) MEANS — that's Ports::IdentityResolution's
job, called by whoever consumes this port's verify.
LAZY REQUIRES, same reasoning Postgres.connect_for already
holds itself to for pg: a domain that never binds
authentication to this adapter should never need these gems
installed. This FILE loads in every boot (driven.rb's own
unconditional require_relative list) ; the GEMS load only where
a real handshake actually happens.
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_REDIRECT_URI —
no defaults, on purpose: a login mechanism silently
half-configured is worse than one that refuses to boot at all.
Constant Summary collapse
- ISSUER =
"https://accounts.google.com"
Class Method Summary collapse
-
.authorization_url ⇒ Object
The URL to send a browser to, carrying a fresh CSRF
statethe caller is responsible for stashing (a session, typically) and checking again inverify. - .client ⇒ Object
-
.verify(code:, state:, expected_state:) ⇒ Object
Exchanges
codefor tokens, verifies the returned ID token's signature and claims against Google's own JWKS, and confirmsstatematches whatauthorization_urlhanded out — a mismatch (or any failure in the exchange/verification itself) is a Ports::Authentication::ValidationError, never a silently-empty result.
Class Method Details
.authorization_url ⇒ Object
The URL to send a browser to, carrying a fresh CSRF state the
caller is responsible for stashing (a session, typically) and
checking again in verify.
50 51 52 53 54 55 56 57 58 |
# File 'lib/hecks/adapters/driven/google_authentication.rb', line 50 def state = SecureRandom.hex(24) url = client.auth_code.( redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI"), scope: "openid email profile", state: state ) [url, state] end |
.client ⇒ Object
37 38 39 40 41 42 43 44 45 |
# File 'lib/hecks/adapters/driven/google_authentication.rb', line 37 def client require "oauth2" OAuth2::Client.new( ENV.fetch("GOOGLE_CLIENT_ID"), ENV.fetch("GOOGLE_CLIENT_SECRET"), site: "https://oauth2.googleapis.com", authorize_url: "https://accounts.google.com/o/oauth2/v2/auth", token_url: "/token" ) end |
.verify(code:, state:, expected_state:) ⇒ Object
Exchanges code for tokens, verifies the returned ID token's
signature and claims against Google's own JWKS, and confirms
state matches what authorization_url handed out — a
mismatch (or any failure in the exchange/verification itself)
is a Ports::Authentication::ValidationError, never a
silently-empty result. Returns
subject:, email:, email_verified: on success — never
the raw token, never anything a caller would need to re-verify
itself. email_verified rides along because a caller granting
access off this email needs to know Google actually checked it.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/hecks/adapters/driven/google_authentication.rb', line 70 def verify(code:, state:, expected_state:) # BOTH GEMS, BEFORE ANYTHING ELSE — not staggered further down # this method: the rescue clause below names GoogleIDToken # ::ValidationError, and Ruby resolves that constant reference # at the moment an exception is being matched, not at parse # time. An OAuth2::Error raised before a later `require # "google-id-token"` line ran would make the rescue clause # itself blow up on an undefined constant, masking the real # error. Requiring both up front means whichever one raises # first, the constant is already there to catch it. require "oauth2" require "google-id-token" unless state && expected_state && state == expected_state raise Ports::Authentication::ValidationError, "state mismatch" end token = client.auth_code.get_token(code, redirect_uri: ENV.fetch("GOOGLE_REDIRECT_URI")) id_token = token.params["id_token"] or raise Ports::Authentication::ValidationError, "no id_token in the response" payload = GoogleIDToken::Validator.new.check(id_token, ENV.fetch("GOOGLE_CLIENT_ID")) { issuer: payload.fetch("iss"), subject: payload.fetch("sub"), email: payload["email"], email_verified: [true, "true"].include?(payload["email_verified"]) } rescue OAuth2::Error, GoogleIDToken::ValidationError => e raise Ports::Authentication::ValidationError, e. end |