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.
- checksums.yaml +4 -4
- data/README.md +162 -15
- data/lib/zoreal/oauth2/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e564eea3a16252cff4698e23df5fbfd5c15f825fffb87924151a1c138b82c3da
|
|
4
|
+
data.tar.gz: a83e1a55610407dee2d652a09da1cce208d9226d644589bd6ff8419b42503135
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
ID token
|
|
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
|
-
- **
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|