zoreal-oauth2 0.1.4 → 0.1.6

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