zoreal-oauth2 0.1.3 → 0.1.4
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 +109 -0
- data/lib/zoreal/oauth2/client.rb +37 -11
- data/lib/zoreal/oauth2/login.rb +15 -0
- 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: 7179440f857aa507bde5dd88b3c5ebf1f714fd73e8ecc3b3c136a6b47f2cc69e
|
|
4
|
+
data.tar.gz: 24f5ad9f699eda4f6b92c99226ce572cd9e015356ec8e11747726ac55d6005c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 145f11a19c406f246ae6258aa7e402a8888882009c3f51d0e694e889aedd6611df04734ab398ff8c6fe51f3940e7b017c78e64bace0072a7cd52dcade5922054
|
|
7
|
+
data.tar.gz: 067e2b2041d54ba943db77313b14821896574414c784a1c629e1726ce966d1b7b76a78a37e3d1a4a249600b93ed1b66ea3c471ad0983ea0304909b9d77795c21
|
data/README.md
CHANGED
|
@@ -67,6 +67,115 @@ if user.nil?
|
|
|
67
67
|
end
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Assurance levels — `acr`, and requiring a liveness check
|
|
71
|
+
|
|
72
|
+
### What `acr` is
|
|
73
|
+
|
|
74
|
+
`acr` is an OpenID Connect standard claim — *Authentication Context Class
|
|
75
|
+
Reference*. It is a single string in the ID token that says **how strongly this
|
|
76
|
+
particular login was authenticated**. Every ZOREAL login carries one, and it is
|
|
77
|
+
the difference between "someone who once enrolled this identity is behind this
|
|
78
|
+
request" and "a live human, verified to be the right one, is behind this request
|
|
79
|
+
right now".
|
|
80
|
+
|
|
81
|
+
It answers a question the `sub` cannot. `sub` tells you *who* (a stable, pairwise
|
|
82
|
+
identifier for this person at your site). `acr` tells you *how sure ZOREAL is that
|
|
83
|
+
the person is really there for this login*. A stolen, unlocked phone can still
|
|
84
|
+
produce a `sub`; it cannot produce a fresh `zoreal.live`.
|
|
85
|
+
|
|
86
|
+
### The three levels
|
|
87
|
+
|
|
88
|
+
Ordered weakest to strongest. Each is what actually happened, never what was
|
|
89
|
+
requested — a login that could only reach a weaker level says so honestly rather
|
|
90
|
+
than claiming the level you asked for.
|
|
91
|
+
|
|
92
|
+
| `acr` | What the holder did | `amr` | What it proves | What it does **not** prove |
|
|
93
|
+
|---|---|---|---|---|
|
|
94
|
+
| `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 |
|
|
95
|
+
| `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 |
|
|
96
|
+
| `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) |
|
|
97
|
+
|
|
98
|
+
`amr` (*Authentication Methods References*) is the companion claim listing the
|
|
99
|
+
factors used: `hwk` a hardware key, `user` a user-presence/unlock gesture, `face`
|
|
100
|
+
a face biometric. `zoreal.live` is exactly `zoreal.device` with `face` added,
|
|
101
|
+
because a live login is a device approval with a capture on top.
|
|
102
|
+
|
|
103
|
+
The **default is `zoreal.device`**, never `zoreal.session`: a login that asks for
|
|
104
|
+
nothing still requires the enrolled phone and a local unlock. Silence has to be
|
|
105
|
+
explicitly asked for (`prompt=none`), and it succeeds only for a returning holder
|
|
106
|
+
at a site whose consent they have already given.
|
|
107
|
+
|
|
108
|
+
### When to require which
|
|
109
|
+
|
|
110
|
+
- **`zoreal.session`** — you never *require* this; it is what a returning holder
|
|
111
|
+
gets for a low-stakes convenience re-auth when they ask for the silent path.
|
|
112
|
+
- **`zoreal.device`** (the default) — a forum, a community, a normal account
|
|
113
|
+
login. Possession of the enrolled phone plus a local unlock is a high bar
|
|
114
|
+
already; most sites want exactly this and should pass no `acr` at all.
|
|
115
|
+
- **`zoreal.live`** — a bank onboarding, a high-value transaction, an age-gated
|
|
116
|
+
purchase, a first login, a "confirm it is really you" step before a sensitive
|
|
117
|
+
action. Anywhere a *fresh, unforgeable proof of the live, right human* is worth
|
|
118
|
+
the few seconds a face capture costs.
|
|
119
|
+
|
|
120
|
+
### Requesting versus verifying — the one rule that matters
|
|
121
|
+
|
|
122
|
+
Requesting a level and verifying it are **two separate steps, and only the second
|
|
123
|
+
is security**:
|
|
124
|
+
|
|
125
|
+
1. **Request** it on the wire, in the frontend, with the SDK's
|
|
126
|
+
`acr_values: 'zoreal.live'`. This is what makes the holder's ZOREAL ID app run
|
|
127
|
+
the face capture before it will approve. It is **advisory** — it shapes what
|
|
128
|
+
the holder is asked to do, nothing more. A browser is attacker-controlled; a
|
|
129
|
+
value that only travels through it proves nothing.
|
|
130
|
+
2. **Verify** it here, at token exchange, by passing `acr:`. The signed `acr`
|
|
131
|
+
claim in the ID token — minted by ZOREAL, not by the browser — is the proof.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
login = ZOREAL_OAUTH.authenticate(
|
|
135
|
+
code: params[:code], code_verifier: params[:code_verifier],
|
|
136
|
+
nonce: params[:nonce],
|
|
137
|
+
acr: 'zoreal.live' # raises VerificationError unless the signed token says so
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
login.acr # "zoreal.live" — what actually happened
|
|
141
|
+
login.live? # convenience: acr == "zoreal.live"
|
|
142
|
+
login.satisfies_acr?('zoreal.device')# true (live is stronger than device)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**An RP that requests `zoreal.live` on the wire but never passes `acr:` here has
|
|
146
|
+
checked nothing** — it has only asked the holder nicely and then trusted a value
|
|
147
|
+
it never validated.
|
|
148
|
+
|
|
149
|
+
### How the check behaves
|
|
150
|
+
|
|
151
|
+
Verification satisfies **upward**: `zoreal.session < zoreal.device <
|
|
152
|
+
zoreal.live`, so a requirement of `zoreal.device` accepts a `zoreal.live` token
|
|
153
|
+
(the holder gave you *more* assurance than you demanded). A token whose `acr` is
|
|
154
|
+
below the requirement, missing entirely, or outside the vocabulary is refused
|
|
155
|
+
with `VerificationError`. An unknown *required* value — a typo like
|
|
156
|
+
`'zoreal.liveness'` — raises `ConfigurationError` instead, because that is a bug
|
|
157
|
+
in your code, not a bad token, and failing every login silently is worse than
|
|
158
|
+
saying so.
|
|
159
|
+
|
|
160
|
+
If you prefer to branch rather than raise, omit `acr:` and inspect the result:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
login = ZOREAL_OAUTH.authenticate(code:, code_verifier:, nonce:)
|
|
164
|
+
unless login.satisfies_acr?('zoreal.live')
|
|
165
|
+
# step the user up, or refuse the sensitive action
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `acr` versus the assurance block
|
|
170
|
+
|
|
171
|
+
Do not confuse `acr` with `login.assurance`. `acr` grades *this login event*.
|
|
172
|
+
The **assurance block** (`login.assurance`) describes the *identity behind it* —
|
|
173
|
+
how the person was verified at enrolment (`uniqueness` basis, `verified_on`
|
|
174
|
+
month, whether chip liveness was proven, the `trust_tier`, the device's
|
|
175
|
+
`key_protection`). One is about now; the other is about who they are. A high-value
|
|
176
|
+
flow usually wants both: `acr: 'zoreal.live'` for presence, and the assurance
|
|
177
|
+
block for the strength of the underlying identity proofing.
|
|
178
|
+
|
|
70
179
|
## Client authentication: all four registered methods
|
|
71
180
|
|
|
72
181
|
| `token_endpoint_auth_method` | Configuration | What travels |
|
data/lib/zoreal/oauth2/client.rb
CHANGED
|
@@ -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
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
|
|
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
|
|
120
|
-
#
|
|
121
|
-
# the claims. There is no RS256
|
|
122
|
-
# nothing with RSA, and accepting a
|
|
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
|
data/lib/zoreal/oauth2/login.rb
CHANGED
|
@@ -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
|