standard_id 0.49.0 → 0.50.0
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/CHANGELOG.md +18 -0
- data/README.md +25 -7
- data/app/controllers/concerns/standard_id/resource_server.rb +4 -0
- data/app/controllers/standard_id/api/authorization_controller.rb +19 -3
- data/lib/standard_id/config/callable_validator.rb +5 -0
- data/lib/standard_id/config/schema.rb +22 -4
- data/lib/standard_id/oauth/audience_resolver.rb +11 -7
- data/lib/standard_id/oauth/authorization_flow.rb +12 -1
- data/lib/standard_id/oauth/authorize_scope_resolver.rb +36 -0
- data/lib/standard_id/oauth/client_registration.rb +44 -9
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6f93f66f3e1a4d95ac6effab590e560e39f2993dd072010676a3121efec6098
|
|
4
|
+
data.tar.gz: 6934ab15f5b9c394db6d15eb37ca91d41965421589553135c2a05fea1ebbf551
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73eab42991c65471c2f77d7be554b4ec1d002eacb73e62d8d77bc79003b5121f4b64bd9750ce8991113ff451556313f54165cfd75b642501251dcb947c0f3900
|
|
7
|
+
data.tar.gz: 6a0b2b9e4628842eb1ab6d56acf348c829abc083a00e29671c3efadca5dc57e56aa63d958577c884a7ca224f6e768aebedc66755d5c1128912fe4ef1d9eed576
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.50.0] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`AudienceResolver`: the client's registered `metadata["audience"]` now outranks the `audience` parameter.** The registered default is set by the server (`dynamic_registration_attributes`) or an operator; letting the request's `audience` beat it meant an openly registered client could ask `/authorize` for another allowed audience, which every consumer's shim had prevented by stripping the parameter. New order: `audience_resolver` hook → registered default → `audience` param → RFC 8707 `resource`. Hosts that set `audience_resolver` to return the registered default are unaffected; a client with no registered default behaves as before.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`c.oauth.authorize_scope_resolver`**: normalise the requested scope at `/authorize` once the client and audience are known and before `audience_scopes` is enforced — collapse a hierarchy, narrow to what the audience consumes, append a default. Callable `(scope:, audience:, client:, request:)` returning a String, Array or nil (unchanged); the answer is what the code is issued with and what consent shows. Validated at boot.
|
|
19
|
+
|
|
20
|
+
## [0.49.1] - 2026-10-20
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Dynamic registration refuses non-https redirect URIs** (`invalid_redirect_uri`): https, or http only on a loopback host (RFC 8252 §7.3), and at most 10 entries — the rules every consumer's own registration operation enforced and the gem's model (which also serves operator-registered clients) does not. Applies to RFC 7592 updates too.
|
|
25
|
+
- **A refused registration no longer leaves an owner row behind**: the `dynamic_registration_owner` callable now runs inside the registration transaction, so a host creating a per-registration owner record gets it rolled back when the client is invalid.
|
|
26
|
+
- **`StandardId::ResourceServer`'s scope check defers to the auth layer**: with no session (the action skipped `verify_access_token!`, e.g. a 405 responder) it no longer raises `NotAuthenticatedError` itself, matching `AudienceVerification`.
|
|
27
|
+
|
|
10
28
|
## [0.49.0] - 2026-10-20
|
|
11
29
|
|
|
12
30
|
### Added
|
data/README.md
CHANGED
|
@@ -1367,9 +1367,9 @@ redirect_to "/api/authorize?" + {
|
|
|
1367
1367
|
|
|
1368
1368
|
**Which audience is the token for?** `/authorize` resolves it server-side
|
|
1369
1369
|
(`StandardId::Oauth::AudienceResolver`), in this order: `config.oauth.audience_resolver`
|
|
1370
|
-
when set; the
|
|
1371
|
-
`
|
|
1372
|
-
`
|
|
1370
|
+
when set; the client's registered default `metadata["audience"]` (server-set, so it
|
|
1371
|
+
outranks the request); the `audience` parameter; the RFC 8707 `resource` parameter
|
|
1372
|
+
mapped through `config.oauth.resource_audiences`. The result is bound to the authorization code and reaches
|
|
1373
1373
|
the token as `aud`, and `config.oauth.audience_scopes` is enforced for it at
|
|
1374
1374
|
`/authorize` (a scope outside the audience's vocabulary is `invalid_scope`). So a
|
|
1375
1375
|
dynamically registered MCP client whose registration stamps
|
|
@@ -1378,10 +1378,28 @@ in front of the endpoint. The `resource` parameter is ignored until
|
|
|
1378
1378
|
`resource_audiences` is set; once it is, an unmapped resource is `invalid_target`
|
|
1379
1379
|
(RFC 8707 §2).
|
|
1380
1380
|
|
|
1381
|
-
|
|
1382
|
-
`audience_scopes` enforcement is only as strong as
|
|
1383
|
-
at the token endpoint.
|
|
1384
|
-
|
|
1381
|
+
For a client with no registered default, a client-supplied `audience` is taken as
|
|
1382
|
+
is, so `audience_scopes` enforcement for such clients is only as strong as
|
|
1383
|
+
`config.oauth.allowed_audiences` at the token endpoint.
|
|
1384
|
+
|
|
1385
|
+
**Which scope?** `config.oauth.authorize_scope_resolver` normalises the requested
|
|
1386
|
+
scope once the client and audience are known and before `audience_scopes` is
|
|
1387
|
+
enforced — collapse a hierarchy, narrow to what the audience consumes, append a
|
|
1388
|
+
default — and its answer is what the code is issued with and the consent screen
|
|
1389
|
+
shows:
|
|
1390
|
+
|
|
1391
|
+
```ruby
|
|
1392
|
+
config.oauth.authorize_scope_resolver = ->(scope:, audience:) {
|
|
1393
|
+
tokens = scope.to_s.split
|
|
1394
|
+
next nil unless audience == "mcp" && tokens.include?("mcp") # nil leaves the request as is
|
|
1395
|
+
|
|
1396
|
+
tokens - ["mcp:read"] # "mcp" already implies read; an Array or a String is fine
|
|
1397
|
+
}
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
It runs again when a consent resumes, so make it idempotent (its own output must
|
|
1401
|
+
come back unchanged); it also decides what the consent screen shows and what the
|
|
1402
|
+
grant records.
|
|
1385
1403
|
|
|
1386
1404
|
```ruby
|
|
1387
1405
|
StandardId.configure do |config|
|
|
@@ -81,8 +81,12 @@ module StandardId
|
|
|
81
81
|
raise(StandardId::ConfigurationError, "#{self.class.name} includes StandardId::ResourceServer but never called protected_resource")
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Like verify_audience!: without a session there is nothing to check, and
|
|
85
|
+
# either the auth layer is answering the 401 or the action skipped
|
|
86
|
+
# verify_access_token! on purpose (a 405 responder, say).
|
|
84
87
|
def verify_protected_resource_scopes!
|
|
85
88
|
return if _protected_resource_scopes.empty?
|
|
89
|
+
return unless current_session
|
|
86
90
|
|
|
87
91
|
require_scopes!(*_protected_resource_scopes)
|
|
88
92
|
end
|
|
@@ -74,7 +74,7 @@ module StandardId
|
|
|
74
74
|
!StandardId::ClientGrant.granted?(
|
|
75
75
|
account: current_account,
|
|
76
76
|
client_id: client.client_id,
|
|
77
|
-
requested_scope:
|
|
77
|
+
requested_scope: consent_scope
|
|
78
78
|
)
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -82,6 +82,21 @@ module StandardId
|
|
|
82
82
|
@consent_client ||= StandardId::ClientApplication.active.find_by(client_id: params[:client_id])
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
# The scope consent is checked against, shown and recorded: the request's,
|
|
86
|
+
# normalised by `oauth.authorize_scope_resolver` exactly as the flow will
|
|
87
|
+
# normalise it when the code is issued, so the screen and the grant never
|
|
88
|
+
# disagree with the token. Needs the client and the resolved audience, so
|
|
89
|
+
# it is computed here rather than inside the flow.
|
|
90
|
+
def consent_scope
|
|
91
|
+
return @consent_scope if defined?(@consent_scope)
|
|
92
|
+
|
|
93
|
+
client = consent_client
|
|
94
|
+
audience = StandardId::Oauth::AudienceResolver.resolve(client: client, params: params, request: request)
|
|
95
|
+
@consent_scope = StandardId::Oauth::AuthorizeScopeResolver.resolve(
|
|
96
|
+
scope: params[:scope], audience: audience, client: client, request: request
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
def redirect_to_consent
|
|
86
101
|
payload = StandardId::Oauth::ConsentPayload.encode(authorize_params_for_resume)
|
|
87
102
|
base = StandardId.config.login_url.present? ? consent_base_from_login_url : "/consent"
|
|
@@ -98,13 +113,14 @@ module StandardId
|
|
|
98
113
|
|
|
99
114
|
# The exact params required to resume authorization-code issuance after
|
|
100
115
|
# approval. Carried through a signed payload so they cannot be tampered
|
|
101
|
-
# with; redirect_uri + PKCE are revalidated when the flow re-runs.
|
|
116
|
+
# with; redirect_uri + PKCE are revalidated when the flow re-runs. The
|
|
117
|
+
# scope is the normalised one the user is shown (see #consent_scope).
|
|
102
118
|
def authorize_params_for_resume
|
|
103
119
|
{
|
|
104
120
|
response_type: response_type,
|
|
105
121
|
client_id: params[:client_id],
|
|
106
122
|
redirect_uri: params[:redirect_uri],
|
|
107
|
-
scope:
|
|
123
|
+
scope: consent_scope,
|
|
108
124
|
audience: params[:audience],
|
|
109
125
|
resource: params[:resource],
|
|
110
126
|
state: params[:state],
|
|
@@ -61,6 +61,11 @@ module StandardId
|
|
|
61
61
|
kind: :keyword,
|
|
62
62
|
keywords: %i[metadata request]
|
|
63
63
|
},
|
|
64
|
+
"oauth.authorize_scope_resolver" => {
|
|
65
|
+
signature: "(scope:, audience:, client:, request:)",
|
|
66
|
+
kind: :keyword,
|
|
67
|
+
keywords: %i[scope audience client request]
|
|
68
|
+
},
|
|
64
69
|
"oauth.audience_scope_resolver" => {
|
|
65
70
|
signature: "(audience:, client:, configured_scopes:)",
|
|
66
71
|
kind: :keyword,
|
|
@@ -421,13 +421,31 @@ StandardId::ConfigSchema.define do
|
|
|
421
421
|
# instead of trusting a client-supplied `audience` alone. Resolution order
|
|
422
422
|
# (StandardId::Oauth::AudienceResolver): this callable when set (any
|
|
423
423
|
# subset of client:, audience:, resource:, request:; a non-nil answer is
|
|
424
|
-
# final), else the
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
# authorization code and reaches the
|
|
424
|
+
# final), else the client's registered default `metadata["audience"]`
|
|
425
|
+
# (server-set, so it outranks the request), else the `audience` param,
|
|
426
|
+
# else the RFC 8707 `resource` param mapped through `resource_audiences`.
|
|
427
|
+
# The resolved audience is bound to the authorization code and reaches the
|
|
428
|
+
# token as `aud`; `audience_scopes`
|
|
428
429
|
# (above) is then enforced at /authorize for it.
|
|
429
430
|
field :audience_resolver, type: :any, default: nil
|
|
430
431
|
|
|
432
|
+
# Normalise the scope an /authorize request asks for, once the client and
|
|
433
|
+
# audience are known and before `audience_scopes` is enforced — the place
|
|
434
|
+
# for what hosts used to do in a shim: collapse a hierarchy ("mcp mcp:read"
|
|
435
|
+
# → "mcp"), narrow a request to what the chosen audience consumes, append
|
|
436
|
+
# a default. A callable taking any subset of `scope:` (the raw param),
|
|
437
|
+
# `audience:` (resolved), `client:` and `request:`; returns the scope as a
|
|
438
|
+
# String or Array, or nil to leave the request unchanged. The result is
|
|
439
|
+
# what the code is issued with, what the consent screen shows and what the
|
|
440
|
+
# grant is checked against and recorded as. It runs again on the consent
|
|
441
|
+
# resume, so it must be idempotent (its own output must come back unchanged).
|
|
442
|
+
#
|
|
443
|
+
# c.oauth.authorize_scope_resolver = ->(scope:, audience:) {
|
|
444
|
+
# tokens = scope.to_s.split
|
|
445
|
+
# audience == "mcp" && tokens.include?("mcp") ? tokens - ["mcp:read"] : nil
|
|
446
|
+
# }
|
|
447
|
+
field :authorize_scope_resolver, type: :any, default: nil
|
|
448
|
+
|
|
431
449
|
# RFC 8707: resource indicator → audience, e.g.
|
|
432
450
|
# { "https://app.example.com/mcp" => "mcp" }. Matched with the trailing
|
|
433
451
|
# slash removed. Empty (the default) means the `resource` parameter is
|
|
@@ -9,9 +9,14 @@ module StandardId
|
|
|
9
9
|
# subset of `client:`, `audience:` (the raw param), `resource:` (the
|
|
10
10
|
# RFC 8707 param) and `request:`. A non-nil answer is final; nil
|
|
11
11
|
# falls through to the defaults below.
|
|
12
|
-
# 2. The `audience`
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# 2. The client's registered default, `metadata["audience"]` — set by the
|
|
13
|
+
# server at registration (`dynamic_registration_attributes`) or by an
|
|
14
|
+
# operator, so it is the one input here the client did not choose, and
|
|
15
|
+
# it outranks what the request says (0.50.0; before that the
|
|
16
|
+
# `audience` parameter came first, which let an openly registered
|
|
17
|
+
# client ask for another allowed audience).
|
|
18
|
+
# 3. The `audience` parameter, when present.
|
|
19
|
+
# 4. The `resource` parameter (RFC 8707), mapped through
|
|
15
20
|
# `config.oauth.resource_audiences` ({ "https://app.example/mcp" =>
|
|
16
21
|
# "mcp" }); compared with the trailing slash removed. With no mapping
|
|
17
22
|
# configured the parameter is ignored (MCP clients send it on every
|
|
@@ -19,9 +24,6 @@ module StandardId
|
|
|
19
24
|
# get that default). With a mapping, an unmapped resource is refused
|
|
20
25
|
# with `invalid_target` (RFC 8707 §2), never silently issued without
|
|
21
26
|
# an audience.
|
|
22
|
-
# 4. The client's registered default, `metadata["audience"]` — the
|
|
23
|
-
# convention every dynamic-registration operation in the estate
|
|
24
|
-
# stamps, and the reason each app carried an authorize shim.
|
|
25
27
|
#
|
|
26
28
|
# Nil when none of these apply: audience stays optional at /authorize.
|
|
27
29
|
module AudienceResolver
|
|
@@ -38,10 +40,12 @@ module StandardId
|
|
|
38
40
|
return coerce_hook_answer(answer) unless answer.nil?
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
registered = client_default(client)
|
|
44
|
+
return registered if registered
|
|
41
45
|
return requested.to_s if requested
|
|
42
46
|
return from_resource(resource) if resource && resource_mapping.any?
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
nil
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
# @raise [StandardId::InvalidTargetError] when a mapping is configured
|
|
@@ -92,8 +92,19 @@ module StandardId
|
|
|
92
92
|
params[:state]
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# The requested scope, normalised by `oauth.authorize_scope_resolver`
|
|
96
|
+
# when set (a host that collapses a scope hierarchy, or narrows a
|
|
97
|
+
# request to what the chosen audience consumes, does it here instead of
|
|
98
|
+
# in a shim in front of this endpoint). Resolved once, after the client
|
|
99
|
+
# and audience are known; nil from the hook leaves the request as is.
|
|
95
100
|
def scope
|
|
96
|
-
|
|
101
|
+
return @scope if defined?(@scope)
|
|
102
|
+
|
|
103
|
+
@scope = resolve_scope(params[:scope])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def resolve_scope(requested)
|
|
107
|
+
StandardId::Oauth::AuthorizeScopeResolver.resolve(scope: requested, audience: audience, client: @client, request: request)
|
|
97
108
|
end
|
|
98
109
|
|
|
99
110
|
# Resolved once, after the client is known (see AudienceResolver): the
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Oauth
|
|
3
|
+
# Applies `config.oauth.authorize_scope_resolver` to an /authorize request's
|
|
4
|
+
# scope once the client and audience are known. Shared by the authorize
|
|
5
|
+
# flows (what the code is issued with) and the consent gate (what the user
|
|
6
|
+
# is shown, what the grant is checked against and recorded as), so the two
|
|
7
|
+
# cannot disagree.
|
|
8
|
+
#
|
|
9
|
+
# The consent resume re-runs the flow with the already-normalised scope, so
|
|
10
|
+
# a resolver must be idempotent: applying it to its own output must change
|
|
11
|
+
# nothing. Collapsing, filtering and appending a default all are; an
|
|
12
|
+
# append that does not check for presence is not (the Array/String output
|
|
13
|
+
# is de-duplicated, which covers the common case).
|
|
14
|
+
module AuthorizeScopeResolver
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @return [String, nil] the normalised scope (nil/blank in → nil/blank out
|
|
18
|
+
# unless the resolver answers)
|
|
19
|
+
def resolve(scope:, audience:, client:, request: nil)
|
|
20
|
+
custom = StandardId.config.oauth.authorize_scope_resolver
|
|
21
|
+
return scope unless custom.respond_to?(:call)
|
|
22
|
+
|
|
23
|
+
context = { scope: scope, audience: audience, client: client, request: request }
|
|
24
|
+
answer = custom.call(**StandardId::Utils::CallableParameterFilter.filter(custom, context))
|
|
25
|
+
case answer
|
|
26
|
+
when nil then scope
|
|
27
|
+
when Array then answer.map(&:to_s).reject(&:blank?).uniq.join(" ")
|
|
28
|
+
when String, Symbol then answer.to_s.split.uniq.join(" ")
|
|
29
|
+
else
|
|
30
|
+
raise StandardId::ConfigurationError,
|
|
31
|
+
"StandardId config: `oauth.authorize_scope_resolver` must return a String, Array, Symbol or nil, got #{answer.class}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -37,6 +37,8 @@ module StandardId
|
|
|
37
37
|
# Keys `config.oauth.dynamic_registration_attributes` may pin.
|
|
38
38
|
PINNABLE_ATTRIBUTES = %w[scopes metadata access_token_lifetime authorization_code_lifetime require_consent description].freeze
|
|
39
39
|
REGISTRATION_ACCESS_TOKEN_BYTES = 32
|
|
40
|
+
# A self-registered client gets a handful of callbacks, not a list.
|
|
41
|
+
MAX_REDIRECT_URIS = 10
|
|
40
42
|
|
|
41
43
|
# Minimal result object mirroring the gem's `result.success?` /
|
|
42
44
|
# `result.value` convention. `client_secret` is the one-time plaintext for
|
|
@@ -88,18 +90,23 @@ module StandardId
|
|
|
88
90
|
client
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
# Everything, the owner resolver included, runs inside one transaction:
|
|
94
|
+
# a host whose resolver creates a per-registration owner row must not be
|
|
95
|
+
# left with that row when the client itself is refused.
|
|
91
96
|
def call
|
|
92
|
-
|
|
97
|
+
validate_redirect_uris!
|
|
93
98
|
registration_access_token = nil
|
|
94
|
-
if StandardId::ClientApplication.registration_management_column?
|
|
95
|
-
registration_access_token = SecureRandom.urlsafe_base64(REGISTRATION_ACCESS_TOKEN_BYTES)
|
|
96
|
-
attrs[:registration_access_token_digest] =
|
|
97
|
-
StandardId::ClientApplication.digest_registration_access_token(registration_access_token)
|
|
98
|
-
end
|
|
99
|
-
client = StandardId::ClientApplication.new(attrs)
|
|
100
|
-
|
|
101
99
|
secret_plaintext = nil
|
|
100
|
+
client = nil
|
|
101
|
+
|
|
102
102
|
StandardId::ClientApplication.transaction do
|
|
103
|
+
attrs = mapped_attributes
|
|
104
|
+
if StandardId::ClientApplication.registration_management_column?
|
|
105
|
+
registration_access_token = SecureRandom.urlsafe_base64(REGISTRATION_ACCESS_TOKEN_BYTES)
|
|
106
|
+
attrs[:registration_access_token_digest] =
|
|
107
|
+
StandardId::ClientApplication.digest_registration_access_token(registration_access_token)
|
|
108
|
+
end
|
|
109
|
+
client = StandardId::ClientApplication.new(attrs)
|
|
103
110
|
client.save!
|
|
104
111
|
if client.confidential?
|
|
105
112
|
secret_plaintext = SecureRandom.hex(32)
|
|
@@ -117,6 +124,7 @@ module StandardId
|
|
|
117
124
|
end
|
|
118
125
|
|
|
119
126
|
def update(client)
|
|
127
|
+
validate_redirect_uris!
|
|
120
128
|
if metadata[:client_id].present? && metadata[:client_id].to_s != client.client_id
|
|
121
129
|
raise StandardId::InvalidClientMetadataError, "client_id does not match the client being updated"
|
|
122
130
|
end
|
|
@@ -191,7 +199,34 @@ module StandardId
|
|
|
191
199
|
# the model and let its redirect-uri validation surface an invalid_redirect_uri
|
|
192
200
|
# error — keeping a single source of truth for URI rules.
|
|
193
201
|
def redirect_uris
|
|
194
|
-
|
|
202
|
+
redirect_uri_list.join(" ")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def redirect_uri_list
|
|
206
|
+
value = metadata[:redirect_uris]
|
|
207
|
+
list = value.is_a?(Array) ? value : value.to_s.split(/\s+/)
|
|
208
|
+
list.map { |u| u.to_s.strip }.reject(&:blank?)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# What the model does not enforce for a client an operator registers by
|
|
212
|
+
# hand, but a self-registering client must meet: https, or http only on a
|
|
213
|
+
# loopback host (RFC 8252 §7.3, for native apps on an ephemeral port), and
|
|
214
|
+
# a bounded list. Absent or empty is left to the model's presence check.
|
|
215
|
+
def validate_redirect_uris!
|
|
216
|
+
list = redirect_uri_list
|
|
217
|
+
return if list.empty?
|
|
218
|
+
|
|
219
|
+
if list.size > MAX_REDIRECT_URIS
|
|
220
|
+
raise StandardId::InvalidRedirectUriError, "redirect_uris must not contain more than #{MAX_REDIRECT_URIS} entries"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
list.each do |uri|
|
|
224
|
+
parsed = StandardId::ClientApplication.parse_redirect_uri(uri)
|
|
225
|
+
next if parsed && (parsed.scheme == "https" || StandardId::ClientApplication.loopback_redirect_uri?(parsed))
|
|
226
|
+
|
|
227
|
+
raise StandardId::InvalidRedirectUriError,
|
|
228
|
+
"redirect_uri #{uri.inspect} must use https (http is allowed only for loopback hosts)"
|
|
229
|
+
end
|
|
195
230
|
end
|
|
196
231
|
|
|
197
232
|
def client_name
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -39,6 +39,7 @@ require "standard_id/account_cleanup"
|
|
|
39
39
|
require "standard_id/oauth/audience_profile_resolver"
|
|
40
40
|
require "standard_id/oauth/audience_scope_resolver"
|
|
41
41
|
require "standard_id/oauth/audience_resolver"
|
|
42
|
+
require "standard_id/oauth/authorize_scope_resolver"
|
|
42
43
|
require "standard_id/oauth/base_request_flow"
|
|
43
44
|
require "standard_id/oauth/token_lifetime_resolver"
|
|
44
45
|
require "standard_id/oauth/oauth_session_persistence"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.50.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/standard_id/oauth/authorization_code_authorization_flow.rb
|
|
288
288
|
- lib/standard_id/oauth/authorization_code_flow.rb
|
|
289
289
|
- lib/standard_id/oauth/authorization_flow.rb
|
|
290
|
+
- lib/standard_id/oauth/authorize_scope_resolver.rb
|
|
290
291
|
- lib/standard_id/oauth/base_request_flow.rb
|
|
291
292
|
- lib/standard_id/oauth/client_credentials_flow.rb
|
|
292
293
|
- lib/standard_id/oauth/client_registration.rb
|