zoreal-oauth2 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -15
  3. data/lib/zoreal/oauth2/version.rb +1 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7179440f857aa507bde5dd88b3c5ebf1f714fd73e8ecc3b3c136a6b47f2cc69e
4
- data.tar.gz: 24f5ad9f699eda4f6b92c99226ce572cd9e015356ec8e11747726ac55d6005c1
3
+ metadata.gz: e564eea3a16252cff4698e23df5fbfd5c15f825fffb87924151a1c138b82c3da
4
+ data.tar.gz: a83e1a55610407dee2d652a09da1cce208d9226d644589bd6ff8419b42503135
5
5
  SHA512:
6
- metadata.gz: 145f11a19c406f246ae6258aa7e402a8888882009c3f51d0e694e889aedd6611df04734ab398ff8c6fe51f3940e7b017c78e64bace0072a7cd52dcade5922054
7
- data.tar.gz: 067e2b2041d54ba943db77313b14821896574414c784a1c629e1726ce966d1b7b76a78a37e3d1a4a249600b93ed1b66ea3c471ad0983ea0304909b9d77795c21
6
+ metadata.gz: fcf40f7e79505d1d392ad956090961ac397de69ab3e21ce90c00a9c7b90f42ff6fd02275612eed1cb2a45c5f11180a28cd020cd69fe68560f615d6f62268b4d2
7
+ data.tar.gz: 192160f6d509783536e587d57965ec9a50df27078d80f47f7cd0881833ef5eb897bf288f55042eb6025c9f2c08f5e3d0a3512e0e4c9d5829143a6ec282321de5
data/README.md CHANGED
@@ -26,6 +26,41 @@ Or directly: `gem install zoreal-oauth2`.
26
26
 
27
27
  Ruby >= 3.1. One dependency: `jwt`.
28
28
 
29
+ ## Getting your credentials
30
+
31
+ Everything the client constructor needs comes from a ZOREAL **asset**.
32
+
33
+ 1. Create an account at **https://zoreal.com** and open **Assets**.
34
+ 2. **Create an asset** — a *website* (a domain you own) or an *app bundle* (a
35
+ reverse-DNS bundle id). An asset is the thing users log in to; its token is
36
+ your `client_id` and it looks like `ast_...`.
37
+ 3. On the asset, open the **OAuth2** tab and set:
38
+ - the **redirect URIs** and **JavaScript origins** your app uses (requests
39
+ from anything not registered are rejected — this is the core control),
40
+ - the **scopes** the client is allowed to request (see the catalogue below),
41
+ - your **client authentication**: generate a **client secret**
42
+ (`client_secret_basic`), or register a **JWKS** for `private_key_jwt`. A
43
+ public client authenticates with PKCE alone and no secret.
44
+ 4. A website asset must **verify its domain** (a DNS or meta-tag proof, shown in
45
+ the dashboard) before it can request personal-data scopes or sign users in;
46
+ the verified domain is what your users' `sub` is pairwise against.
47
+
48
+ The `client_id` is public (it ships in your frontend). The client secret is not
49
+ — keep it in your server's secret store, never in the browser.
50
+
51
+ ### There is no test-identity sandbox — and that is deliberate
52
+
53
+ ZOREAL **never issues fake or sandbox humans**: a pool of test identities would
54
+ be a fraud vector against the exact thing the product proves. So you always
55
+ authenticate **real** ZOREAL IDs.
56
+
57
+ To develop and test, **create a free ZOREAL ID for yourself** (enrol in the
58
+ ZOREAL ID app) and sign in with it. Mark your asset's environment **sandbox**
59
+ in the dashboard while building — a sandbox asset may register `http://localhost`
60
+ origins and redirect URIs that a production asset may not — and flip it to
61
+ production when you ship. The identities are real either way; only the allowed
62
+ origins differ.
63
+
29
64
  ## Quick start
30
65
 
31
66
  Build one client at boot and share it; it is thread-safe.
@@ -205,10 +240,123 @@ a `private_key` implies `private_key_jwt`, neither means `none`.
205
240
  and `portrait` from `/userinfo` (`portrait` is registrable but not served by
206
241
  the provider yet, and returns nil until it is).
207
242
 
208
- Errors: `ConfigurationError`, `ExchangeError` (carries the provider's OAuth
209
- error code and reason, verbatim), `VerificationError`, `UserinfoError`. A
210
- returning user matched on `sub` can survive a rescued `UserinfoError`; a
211
- signup that needs the email cannot.
243
+ ## Scopes and claims
244
+
245
+ Scopes are requested in the **frontend** (the SDK's `scope` string, always
246
+ starting with `openid`), consented to by the holder, and pre-authorized on your
247
+ asset. What each grants and where it is delivered:
248
+
249
+ | Scope | Claims | Delivered in | Tier | Requires |
250
+ |---|---|---|---|---|
251
+ | `openid` | `sub`, `iss`, `aud`, `exp`, `iat`, `nonce`, `auth_time`, `acr`, `amr`, and the assurance block | ID token | A | any client |
252
+ | `zoreal.age` | `age_over_13/16/18/21/65` booleans — only the thresholds you registered, never an age or birthdate | ID token | A | any client |
253
+ | `zoreal.nationality` | `nationality` (ISO 3166-1 alpha-3) | ID token | A | any client |
254
+ | `email` | `email`, `email_verified` | `/userinfo` | B | confidential client + verified domain |
255
+ | `profile.name` | `name`, `given_name`, `family_name` | `/userinfo` | B | confidential client + verified domain |
256
+ | `profile.birthdate` | `birthdate` (full ISO 8601 date) | `/userinfo` | B | confidential client + verified domain |
257
+ | `profile.document` | `document_type`, `document_number`, `issuing_country`, `document_expires_on` | `/userinfo` | B | confidential client + verified domain |
258
+ | `profile.portrait` | `portrait` (the chip's facial image; GDPR Article 9 data) | `/userinfo` | C | confidential client + verified domain — *registrable but not served yet* |
259
+
260
+ - **Tier A** rides in the ID token and is available to every client, so the
261
+ no-backend browser button can use it. **Tier B and C** are personal data,
262
+ served only from `/userinfo` to a confidential client on a domain you have
263
+ verified, and never placed in a browser token.
264
+ - **Age thresholds are a fixed set** — 13, 16, 18, 21, 65 — that you register on
265
+ the asset. `login.age_over?(n)` returns `nil` for a threshold you did not
266
+ register (no claim was minted), which is different from `false`.
267
+
268
+ ## Error reference
269
+
270
+ `exchange` / `authenticate` raise `ExchangeError`, which carries the provider's
271
+ own `oauth_error` code and `description` verbatim. What you will actually see:
272
+
273
+ | `oauth_error` | Cause | Retryable? |
274
+ |---|---|---|
275
+ | `invalid_grant` | The code is spent — unknown, expired (60s), already used, PKCE mismatch, or the asset's domain verification lapsed mid-flow | No. Start a **new** login; the code cannot be reused |
276
+ | `invalid_request` | Client authentication failed — wrong secret, a bad `private_key_jwt` assertion, or `tls_client_auth` (not accepted at `/token` yet) | No. Fix your client configuration |
277
+ | `unsupported_grant_type` | Something other than `authorization_code` reached `/token` | No. A bug |
278
+
279
+ Errors that surface in the **frontend** instead, before your backend is
280
+ involved (from the SDK's `onError` / `onNonOAuthError`), so handle them there:
281
+
282
+ | Where | Code | Meaning |
283
+ |---|---|---|
284
+ | `/pair` | `invalid_scope` | A scope not on the asset's allowed list, or a Tier B scope from a public client |
285
+ | `/pair` | `invalid_request` | Missing PKCE/nonce, an unverified sector, an unregistered `redirect_uri`, or an unknown `acr_values` |
286
+ | `/pair` | `login_required` | `prompt=none` with no silent session to resume — the expected quiet outcome, not a failure |
287
+ | pairing | `request_denied` | The holder declined in their ZOREAL ID app — **not an error to alarm on**; offer to try again |
288
+ | pairing | `request_expired` | The pairing window elapsed, or a required liveness the device could not meet — offer to try again |
289
+
290
+ The gem's other error classes: `ConfigurationError` (you built the client wrong,
291
+ or asked to verify an acr outside the vocabulary — a bug in your code, not a bad
292
+ token), `VerificationError` (the ID token did not verify: signature, `iss`,
293
+ `aud`, `exp`, `nonce`, or the acr floor), and `UserinfoError` (the `/userinfo`
294
+ call failed). A returning user matched on `sub` can survive a rescued
295
+ `UserinfoError`; a signup that needs the email cannot.
296
+
297
+ ## The assurance block
298
+
299
+ `login.assurance` is the ID token's `zoreal` claim — a hash describing the
300
+ strength of the *identity* behind this login (distinct from `acr`, which grades
301
+ the *login event*). Its keys and their value sets:
302
+
303
+ | Key | Values | Meaning |
304
+ |---|---|---|
305
+ | `uniqueness` | `personal_number` \| `document` \| `none` | The anchor the holder is deduplicated on. `personal_number` (a national number from the chip) is strongest; `none` means no reliable anchor |
306
+ | `verified_on` | `"YYYY-MM"` | The month the underlying document was verified. Quantised to a month on purpose — a day-precision date is a cross-site correlator |
307
+ | `chip_liveness_proven` | `true` \| `false` | Whether the passport chip's active-authentication challenge was proven (a genuine chip, not a clone) |
308
+ | `trust_tier` | `high` \| `standard` | `high` when `chip_liveness_proven`, else `standard` |
309
+ | `key_protection` | `secure_enclave` \| `strongbox` \| `tee` \| `software` | How the holder's device key is protected. `software` means no hardware attestation |
310
+
311
+ A high-value flow usually pairs `acr: 'zoreal.live'` (fresh presence) with a
312
+ check on the assurance block (identity strength) — e.g. requiring
313
+ `uniqueness == 'personal_number'` and `trust_tier == 'high'`.
314
+
315
+ ## A complete example
316
+
317
+ A Rails controller, end to end — the shape a real integration takes:
318
+
319
+ ```ruby
320
+ # config/routes.rb
321
+ post '/auth/zoreal', to: 'sessions#zoreal'
322
+
323
+ # app/controllers/sessions_controller.rb
324
+ class SessionsController < ApplicationController
325
+ # Your frontend's ZorealLogin onSuccess posts { code, code_verifier, nonce }
326
+ # here over your own TLS. Protect this endpoint with your normal CSRF /
327
+ # same-origin controls, exactly as you would any login endpoint — the ZOREAL
328
+ # nonce protects the token, not your route.
329
+ def zoreal
330
+ login = ZOREAL_OAUTH.authenticate(
331
+ code: params[:code],
332
+ code_verifier: params[:code_verifier],
333
+ nonce: params[:nonce]
334
+ # acr: 'zoreal.live' # add for a step-up / high-value login
335
+ )
336
+
337
+ user = User.find_by(provider: 'zoreal', uid: login.sub)
338
+ if user.nil?
339
+ # Claim an existing account that owns this verified email rather than
340
+ # colliding on the unique index; otherwise create one.
341
+ user = User.find_by(email: login.email) if login.email_verified?
342
+ user ||= User.new(email: login.email, full_name: login.name)
343
+ user.update!(provider: 'zoreal', uid: login.sub)
344
+ end
345
+
346
+ reset_session # fixation defence
347
+ session[:user_id] = user.id
348
+ render json: { ok: true }
349
+ rescue Zoreal::OAuth2::ExchangeError, Zoreal::OAuth2::VerificationError => e
350
+ # A spent code or a token that did not verify: the login must be restarted.
351
+ Rails.logger.warn("ZOREAL login failed: #{e.message}")
352
+ render json: { error: 'sign_in_failed' }, status: :unauthorized
353
+ rescue Zoreal::OAuth2::UserinfoError => e
354
+ # Personal data was unreachable. Fine for a returning user matched on sub;
355
+ # fatal for a signup that needs the email.
356
+ render json: { error: 'sign_in_failed' }, status: :unauthorized
357
+ end
358
+ end
359
+ ```
212
360
 
213
361
  ## Things worth knowing before you integrate
214
362
 
@@ -223,20 +371,19 @@ signup that needs the email cannot.
223
371
  rotates every `sub` you have stored. Plan domain changes as a migration.
224
372
  - **ES256 only.** The provider signs with nothing else, and this gem refuses
225
373
  other algorithms rather than negotiating.
226
- - **Always pass the nonce through.** The SDK generates it and gives it to your
227
- frontend in `onSuccess`; without it your backend cannot tell a substituted
228
- ID token from the real one.
374
+ - **Always pass the nonce through, and protect your own endpoint too.** The SDK
375
+ generates the nonce and gives it to your frontend in `onSuccess`; passing it
376
+ here lets the gem confirm the ID token was minted for *this* login rather than
377
+ substituted. Two things it does **not** do: it is not your endpoint's CSRF
378
+ token (protect your `/auth/zoreal` route with your framework's normal CSRF /
379
+ same-origin defence), and PKCE — not the nonce — is what proves whoever
380
+ exchanges the code is whoever started the flow.
229
381
  - **Email is a deliberate choice.** It is a Tier B scope precisely because a
230
382
  shared email defeats the unlinkability the pairwise `sub` provides. Request
231
383
  it because you need it, not because the checkbox is familiar.
232
- - **Sandbox clients accept localhost origins; production clients do not.**
233
- Registration lives in the ZOREAL dashboard on the asset's OAuth2 tab; Tier B
234
- scopes (email, profile.\*) need a confidential client on a verified domain.
235
-
236
- ## Development against a local provider
237
-
238
- Point `issuer:` at your provider instance. The issuer value must match the `iss` inside the
239
- tokens exactly — it is compared, not normalized.
384
+ - **The `issuer` must match the token's `iss` exactly** — it is compared, not
385
+ normalized. Production is `https://id.zoreal.com`; override `issuer:` only
386
+ when pointing at a non-production provider you were given.
240
387
 
241
388
  ## The ZOREAL OAuth2 library family
242
389
 
@@ -1,5 +1,5 @@
1
1
  module Zoreal
2
2
  module OAuth2
3
- VERSION = '0.1.4'.freeze
3
+ VERSION = '0.1.5'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zoreal-oauth2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZOREAL