zoreal-oauth2 0.1.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 316d86bac36a690c10fdfefbf5b5f1ebbadce45e56e69d0d0074d641ff9e1f07
4
- data.tar.gz: c47f42263f881132a9281b70402654bb4f07bfa9b7963544a712e4f4ee8c8246
3
+ metadata.gz: e564eea3a16252cff4698e23df5fbfd5c15f825fffb87924151a1c138b82c3da
4
+ data.tar.gz: a83e1a55610407dee2d652a09da1cce208d9226d644589bd6ff8419b42503135
5
5
  SHA512:
6
- metadata.gz: c2341f70010a3769490afe488816d3a223b243693c1bdf70ffbfd1f7d0ce81443ba55c32126bb3f9629232530ae3590d3c85ab0a5f3820c0cc48b4a78fb67c61
7
- data.tar.gz: 10447f93e400fb9154fe8e59a46655640ed7f995c39c54b23fb23ebbbbf134c650ad278dc2e1ed4904de39afec1873cd3b0f284a4a98e7dce843981fd478339f
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.
@@ -67,6 +102,115 @@ if user.nil?
67
102
  end
68
103
  ```
69
104
 
105
+ ## Assurance levels — `acr`, and requiring a liveness check
106
+
107
+ ### What `acr` is
108
+
109
+ `acr` is an OpenID Connect standard claim — *Authentication Context Class
110
+ Reference*. It is a single string in the ID token that says **how strongly this
111
+ particular login was authenticated**. Every ZOREAL login carries one, and it is
112
+ the difference between "someone who once enrolled this identity is behind this
113
+ request" and "a live human, verified to be the right one, is behind this request
114
+ right now".
115
+
116
+ It answers a question the `sub` cannot. `sub` tells you *who* (a stable, pairwise
117
+ identifier for this person at your site). `acr` tells you *how sure ZOREAL is that
118
+ the person is really there for this login*. A stolen, unlocked phone can still
119
+ produce a `sub`; it cannot produce a fresh `zoreal.live`.
120
+
121
+ ### The three levels
122
+
123
+ Ordered weakest to strongest. Each is what actually happened, never what was
124
+ requested — a login that could only reach a weaker level says so honestly rather
125
+ than claiming the level you asked for.
126
+
127
+ | `acr` | What the holder did | `amr` | What it proves | What it does **not** prove |
128
+ |---|---|---|---|---|
129
+ | `zoreal.session` | Nothing — a returning holder at a site they have used before, resumed silently from an existing ZOREAL session, no phone interaction | `[]` | Continuity: the same browser/session ZOREAL already knew | That the holder is present, or even awake |
130
+ | `zoreal.device` | Approved the login on their enrolled phone: a signature from a key in the phone's secure element, released by a local biometric or passcode unlock | `["hwk","user"]` | Possession of the enrolled device **and** a local unlock on it | That a live face was captured for *this* login — an unlocked phone in the wrong hands still signs |
131
+ | `zoreal.live` | All of the above **plus** a fresh face capture this login: a flash-plus-zoom video scored for presentation attacks and screen replay (moire), matched 1:1 against the government document read at enrolment | `["hwk","face","user"]` | A live, real, unique human, verified to be the enrolled person, **at the moment of this login** | — (this is the strongest level) |
132
+
133
+ `amr` (*Authentication Methods References*) is the companion claim listing the
134
+ factors used: `hwk` a hardware key, `user` a user-presence/unlock gesture, `face`
135
+ a face biometric. `zoreal.live` is exactly `zoreal.device` with `face` added,
136
+ because a live login is a device approval with a capture on top.
137
+
138
+ The **default is `zoreal.device`**, never `zoreal.session`: a login that asks for
139
+ nothing still requires the enrolled phone and a local unlock. Silence has to be
140
+ explicitly asked for (`prompt=none`), and it succeeds only for a returning holder
141
+ at a site whose consent they have already given.
142
+
143
+ ### When to require which
144
+
145
+ - **`zoreal.session`** — you never *require* this; it is what a returning holder
146
+ gets for a low-stakes convenience re-auth when they ask for the silent path.
147
+ - **`zoreal.device`** (the default) — a forum, a community, a normal account
148
+ login. Possession of the enrolled phone plus a local unlock is a high bar
149
+ already; most sites want exactly this and should pass no `acr` at all.
150
+ - **`zoreal.live`** — a bank onboarding, a high-value transaction, an age-gated
151
+ purchase, a first login, a "confirm it is really you" step before a sensitive
152
+ action. Anywhere a *fresh, unforgeable proof of the live, right human* is worth
153
+ the few seconds a face capture costs.
154
+
155
+ ### Requesting versus verifying — the one rule that matters
156
+
157
+ Requesting a level and verifying it are **two separate steps, and only the second
158
+ is security**:
159
+
160
+ 1. **Request** it on the wire, in the frontend, with the SDK's
161
+ `acr_values: 'zoreal.live'`. This is what makes the holder's ZOREAL ID app run
162
+ the face capture before it will approve. It is **advisory** — it shapes what
163
+ the holder is asked to do, nothing more. A browser is attacker-controlled; a
164
+ value that only travels through it proves nothing.
165
+ 2. **Verify** it here, at token exchange, by passing `acr:`. The signed `acr`
166
+ claim in the ID token — minted by ZOREAL, not by the browser — is the proof.
167
+
168
+ ```ruby
169
+ login = ZOREAL_OAUTH.authenticate(
170
+ code: params[:code], code_verifier: params[:code_verifier],
171
+ nonce: params[:nonce],
172
+ acr: 'zoreal.live' # raises VerificationError unless the signed token says so
173
+ )
174
+
175
+ login.acr # "zoreal.live" — what actually happened
176
+ login.live? # convenience: acr == "zoreal.live"
177
+ login.satisfies_acr?('zoreal.device')# true (live is stronger than device)
178
+ ```
179
+
180
+ **An RP that requests `zoreal.live` on the wire but never passes `acr:` here has
181
+ checked nothing** — it has only asked the holder nicely and then trusted a value
182
+ it never validated.
183
+
184
+ ### How the check behaves
185
+
186
+ Verification satisfies **upward**: `zoreal.session < zoreal.device <
187
+ zoreal.live`, so a requirement of `zoreal.device` accepts a `zoreal.live` token
188
+ (the holder gave you *more* assurance than you demanded). A token whose `acr` is
189
+ below the requirement, missing entirely, or outside the vocabulary is refused
190
+ with `VerificationError`. An unknown *required* value — a typo like
191
+ `'zoreal.liveness'` — raises `ConfigurationError` instead, because that is a bug
192
+ in your code, not a bad token, and failing every login silently is worse than
193
+ saying so.
194
+
195
+ If you prefer to branch rather than raise, omit `acr:` and inspect the result:
196
+
197
+ ```ruby
198
+ login = ZOREAL_OAUTH.authenticate(code:, code_verifier:, nonce:)
199
+ unless login.satisfies_acr?('zoreal.live')
200
+ # step the user up, or refuse the sensitive action
201
+ end
202
+ ```
203
+
204
+ ### `acr` versus the assurance block
205
+
206
+ Do not confuse `acr` with `login.assurance`. `acr` grades *this login event*.
207
+ The **assurance block** (`login.assurance`) describes the *identity behind it* —
208
+ how the person was verified at enrolment (`uniqueness` basis, `verified_on`
209
+ month, whether chip liveness was proven, the `trust_tier`, the device's
210
+ `key_protection`). One is about now; the other is about who they are. A high-value
211
+ flow usually wants both: `acr: 'zoreal.live'` for presence, and the assurance
212
+ block for the strength of the underlying identity proofing.
213
+
70
214
  ## Client authentication: all four registered methods
71
215
 
72
216
  | `token_endpoint_auth_method` | Configuration | What travels |
@@ -96,10 +240,123 @@ a `private_key` implies `private_key_jwt`, neither means `none`.
96
240
  and `portrait` from `/userinfo` (`portrait` is registrable but not served by
97
241
  the provider yet, and returns nil until it is).
98
242
 
99
- Errors: `ConfigurationError`, `ExchangeError` (carries the provider's OAuth
100
- error code and reason, verbatim), `VerificationError`, `UserinfoError`. A
101
- returning user matched on `sub` can survive a rescued `UserinfoError`; a
102
- 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
+ ```
103
360
 
104
361
  ## Things worth knowing before you integrate
105
362
 
@@ -114,20 +371,19 @@ signup that needs the email cannot.
114
371
  rotates every `sub` you have stored. Plan domain changes as a migration.
115
372
  - **ES256 only.** The provider signs with nothing else, and this gem refuses
116
373
  other algorithms rather than negotiating.
117
- - **Always pass the nonce through.** The SDK generates it and gives it to your
118
- frontend in `onSuccess`; without it your backend cannot tell a substituted
119
- 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.
120
381
  - **Email is a deliberate choice.** It is a Tier B scope precisely because a
121
382
  shared email defeats the unlinkability the pairwise `sub` provides. Request
122
383
  it because you need it, not because the checkbox is familiar.
123
- - **Sandbox clients accept localhost origins; production clients do not.**
124
- Registration lives in the ZOREAL dashboard on the asset's OAuth2 tab; Tier B
125
- scopes (email, profile.\*) need a confidential client on a verified domain.
126
-
127
- ## Development against a local provider
128
-
129
- Point `issuer:` at your provider instance. The issuer value must match the `iss` inside the
130
- 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.
131
387
 
132
388
  ## The ZOREAL OAuth2 library family
133
389
 
@@ -30,6 +30,10 @@ module Zoreal
30
30
  JWKS_CACHE_KEY = 'zoreal_oauth2_jwks'.freeze
31
31
 
32
32
  AUTH_METHODS = %w[none client_secret_basic private_key_jwt tls_client_auth].freeze
33
+ # The assurance vocabulary, weakest to strongest. Verification accepts
34
+ # equal or stronger: an RP requiring zoreal.device is satisfied by a
35
+ # zoreal.live token, never the reverse.
36
+ ACR_ORDER = { 'zoreal.session' => 0, 'zoreal.device' => 1, 'zoreal.live' => 2 }.freeze
33
37
  # The provider rejects an assertion whose exp is more than 60 seconds
34
38
  # out, so that is the lifetime, not a choice.
35
39
  ASSERTION_LIFETIME = 60
@@ -80,12 +84,19 @@ module Zoreal
80
84
 
81
85
  # The whole login, in order: exchange the code (with the PKCE verifier
82
86
  # the browser SDK handed over), verify the ID token against the JWKS,
83
- # check the nonce when the caller has it. Returns a Login; personal data
84
- # is NOT fetched here, because the ID token never carries it and not
85
- # every caller wants it Login#userinfo fetches on first use.
86
- def authenticate(code:, code_verifier:, nonce: nil)
87
+ # check the nonce when the caller has it, and when the caller passes
88
+ # acr: refuse a token whose assurance is below it. Returns a Login;
89
+ # personal data is NOT fetched here, because the ID token never carries
90
+ # it and not every caller wants it — Login#userinfo fetches on first use.
91
+ #
92
+ # REQUESTING an assurance on the wire (the SDK's acr_values) is
93
+ # advisory; the signed acr claim is the proof, and this parameter is
94
+ # where a relying party that asked for a liveness check verifies it
95
+ # actually happened. An RP that requires zoreal.live and never passes
96
+ # acr: here has checked nothing.
97
+ def authenticate(code:, code_verifier:, nonce: nil, acr: nil)
87
98
  tokens = exchange(code: code, code_verifier: code_verifier)
88
- claims = verify_id_token(tokens['id_token'], nonce: nonce)
99
+ claims = verify_id_token(tokens['id_token'], nonce: nonce, acr: acr)
89
100
  Login.new(client: self, claims: claims,
90
101
  id_token: tokens['id_token'],
91
102
  access_token: tokens['access_token'],
@@ -116,12 +127,12 @@ module Zoreal
116
127
  body
117
128
  end
118
129
 
119
- # ES256 against the provider's JWKS, plus iss, aud, exp and — when the
120
- # caller passes the nonce the SDK generated the nonce binding. Returns
121
- # the claims. There is no RS256 fallback on purpose: ZOREAL signs
122
- # nothing with RSA, and accepting a second algorithm is how algorithm
123
- # confusion starts.
124
- def verify_id_token(id_token, nonce: nil)
130
+ # ES256 against the provider's JWKS, plus iss, aud, exp, the nonce
131
+ # binding when the caller has the nonce, and the assurance floor when
132
+ # the caller passes acr:. Returns the claims. There is no RS256
133
+ # fallback on purpose: ZOREAL signs nothing with RSA, and accepting a
134
+ # second algorithm is how algorithm confusion starts.
135
+ def verify_id_token(id_token, nonce: nil, acr: nil)
125
136
  claims, = JWT.decode(
126
137
  id_token, nil, true,
127
138
  algorithms: ['ES256'],
@@ -135,6 +146,7 @@ module Zoreal
135
146
  if !nil_or_empty?(nonce) && claims['nonce'] != nonce
136
147
  raise VerificationError, 'the ID token nonce is not the one this login started with'
137
148
  end
149
+ verify_acr!(claims, acr) unless nil_or_empty?(acr)
138
150
 
139
151
  claims
140
152
  rescue JWT::DecodeError => e
@@ -162,6 +174,20 @@ module Zoreal
162
174
 
163
175
  private
164
176
 
177
+ # Equal or stronger satisfies; anything else — weaker, missing, or a
178
+ # value outside the vocabulary — is refused. An unknown REQUIREMENT is a
179
+ # caller bug and says so plainly rather than failing every login.
180
+ def verify_acr!(claims, required)
181
+ required_rank = ACR_ORDER[required]
182
+ raise ConfigurationError, "unknown required acr #{required}; supported: #{ACR_ORDER.keys.join(', ')}" if required_rank.nil?
183
+
184
+ actual_rank = ACR_ORDER[claims['acr']]
185
+ return if actual_rank && actual_rank >= required_rank
186
+
187
+ raise VerificationError,
188
+ "the ID token says acr #{claims['acr'].inspect}, below the required #{required}"
189
+ end
190
+
165
191
  def jwks
166
192
  cached = @cache.read(JWKS_CACHE_KEY)
167
193
  return cached if cached
@@ -31,6 +31,21 @@ module Zoreal
31
31
  claims['acr']
32
32
  end
33
33
 
34
+ # A fresh liveness capture backed this login. The convenience spelling
35
+ # of acr == 'zoreal.live'; for enforcement, pass acr: to authenticate
36
+ # and let verification refuse the token instead of checking after.
37
+ def live?
38
+ acr == 'zoreal.live'
39
+ end
40
+
41
+ # Equal or stronger satisfies, on the client's ordering
42
+ # (session < device < live). Unknown values satisfy nothing.
43
+ def satisfies_acr?(required)
44
+ actual = Client::ACR_ORDER[acr]
45
+ wanted = Client::ACR_ORDER[required]
46
+ !actual.nil? && !wanted.nil? && actual >= wanted
47
+ end
48
+
34
49
  def amr
35
50
  claims['amr']
36
51
  end
@@ -1,5 +1,5 @@
1
1
  module Zoreal
2
2
  module OAuth2
3
- VERSION = '0.1.3'.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.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZOREAL