better_auth-oauth-provider 0.6.2 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e9330bd8c9a574651d4170f22486e66afc7480f30c9f004f02c0dbd2d366d4e
4
- data.tar.gz: 2f6022d74c0a07ce4eb1e9a8ef05558478fa798c21567b8cf6ba5284b296f5d0
3
+ metadata.gz: a0b02bfe381c6895d73aca8a1084ee6e540da6e5f7d4bad032729d4695932e30
4
+ data.tar.gz: a24ba3d9aed24d1e65d338bfd2bf1d1627f2e4f6f78e24b6d44a2a7f6a973e55
5
5
  SHA512:
6
- metadata.gz: c9abcc25c88f25d6f340afa4a44a4332d9d057b8ddf956542673c8c7d5ff59b29482d86db1dc33ddb3f5c287d4ac6e8fd2cea4873120360a4fab4cde3791a95b
7
- data.tar.gz: 9971e847c0c74fbd1b3493c561748a8ee0d1e04c2f3179157215cacafbc545836cb7eca139cbb6589cca2a44d4cc584ccf327253323f9a91b3caea9eb6bcc5da
6
+ metadata.gz: 6cebc495d6cef993a9d948b0f3bfe41706d26147b9b2f8d90b540ed4d2782ab28db1eaa23321d00ad76cb5aa36c5f41b8d74a5532cce4415e3620b0bffac8751
7
+ data.tar.gz: 1ee7480850340bb18bdccd9e933c3c03b15e168b33d3d3511525be5e6882473f05864d58c5d9e37559d86da906f157859e35861ca834192d35efe1e263d19c6b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0 - 2026-05-05
6
+
7
+ - Fixed OAuth provider consent approval, metadata, issuer normalization, revocation persistence, and endpoint-specific rate limits for hardening parity.
8
+ - Changed RP-initiated logout ID token validation to use the hardened HS256 ID token key; old ID tokens signed only with the public client id will no longer validate.
9
+ - Hardened OAuth client endpoints, token exchange, introspection, userinfo, and pairwise behavior with expanded parity coverage.
10
+
5
11
  ## 0.3.0 - 2026-04-30
6
12
 
7
13
  - Added upstream-parity support for provider init validation, request URI resolution, prompt handling, consent reference IDs, client references, custom token/id-token claims, scope-specific access-token expiry, M2M token defaults, userinfo JWT verification, and expanded introspection fields.
data/README.md CHANGED
@@ -57,6 +57,7 @@ tokens = auth.api.o_auth2_token(
57
57
  ```
58
58
 
59
59
  When `resource` is present and valid, access tokens are JWTs. Without `resource`, access tokens are opaque and introspectable.
60
+ By default, valid JWT access-token audiences are the provider issuer URL and, when `openid` is granted, the UserInfo endpoint. Set `valid_audiences` to allow additional resource servers.
60
61
 
61
62
  ## Routes
62
63
 
@@ -105,11 +106,18 @@ Common options accepted by `BetterAuth::Plugins.oauth_provider`:
105
106
  - `allow_unauthenticated_client_registration`
106
107
  - `client_registration_default_scopes`
107
108
  - `client_registration_allowed_scopes`
109
+ - `client_credential_grant_default_scopes`
108
110
  - `store_client_secret`
109
111
  - `prefix`
112
+ - `code_expires_in`
113
+ - `id_token_expires_in`
110
114
  - `refresh_token_expires_in`
115
+ - `access_token_expires_in`
116
+ - `m2m_access_token_expires_in`
117
+ - `scope_expirations`
111
118
  - `advertised_metadata`
112
119
  - `valid_audiences`
120
+ - `allow_public_client_prelogin`
113
121
  - `custom_token_response_fields`
114
122
  - `custom_access_token_claims`
115
123
  - `custom_user_info_claims`
@@ -158,7 +166,9 @@ Then drop the legacy access-token and consent columns. Rails apps using `better_
158
166
 
159
167
  ## Ruby Adaptations
160
168
 
161
- When the JWT plugin is registered, OIDC metadata advertises its configured `jwks.key_pair_config.alg`, defaulting to `EdDSA` like upstream. If `disable_jwt_plugin: true` is set, Ruby intentionally falls back to HS256 ID tokens signed with the Better Auth secret.
169
+ When the JWT plugin is registered, JWT access tokens and ID tokens use the JWT plugin's configured `jwks.key_pair_config.alg`, defaulting to `EdDSA` like upstream. If the JWT plugin is not registered, or `disable_jwt_plugin: true` is set, Ruby intentionally falls back to HS256 for compatibility.
170
+
171
+ Upstream `oauthProviderResourceClient` and MCP protected-resource helpers remain future API-boundary work for Ruby. This gem currently hardens authorization-server behavior only.
162
172
 
163
173
  Route OpenAPI metadata blocks from upstream TypeScript are intentionally not ported into this package. Use the Ruby `open_api` plugin for generated OpenAPI output.
164
174
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module OAuthProvider
5
- VERSION = "0.6.2"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
@@ -6,11 +6,15 @@ module BetterAuth
6
6
 
7
7
  def oauth_consent_endpoint(config)
8
8
  Endpoint.new(path: "/oauth2/consent", method: "POST") do |ctx|
9
- current_session = Routes.current_session(ctx)
9
+ current_session = Routes.current_session(ctx, allow_nil: true)
10
10
  body = OAuthProtocol.stringify_keys(ctx.body)
11
11
  consent = config[:store][:consents].delete(body["consent_code"].to_s)
12
12
  raise APIError.new("BAD_REQUEST", message: "invalid consent_code") unless consent
13
13
  raise APIError.new("BAD_REQUEST", message: "expired consent_code") if consent[:expires_at] <= Time.now
14
+ raise APIError.new("UNAUTHORIZED", message: "session required") unless current_session
15
+ unless current_session[:user]["id"].to_s == consent[:session][:user]["id"].to_s
16
+ raise APIError.new("FORBIDDEN", message: "consent session mismatch")
17
+ end
14
18
 
15
19
  query = consent[:query]
16
20
  if body["accept"] == false || body["accept"].to_s == "false"
@@ -24,7 +28,7 @@ module BetterAuth
24
28
  raise APIError.new("BAD_REQUEST", message: "invalid_scope")
25
29
  end
26
30
 
27
- reference_id = oauth_consent_reference(config, current_session, granted_scopes) || consent[:reference_id]
31
+ reference_id = consent[:reference_id]
28
32
  oauth_store_consent(ctx, consent[:client], consent[:session], granted_scopes, reference_id)
29
33
  redirect = oauth_authorization_redirect(ctx, config, query, consent[:session], consent[:client], granted_scopes, reference_id: reference_id)
30
34
  ctx.json({redirectURI: redirect})
@@ -44,7 +48,8 @@ module BetterAuth
44
48
  code_challenge: query["code_challenge"],
45
49
  code_challenge_method: query["code_challenge_method"],
46
50
  nonce: query["nonce"],
47
- reference_id: reference_id || client_reference_id
51
+ reference_id: reference_id || client_reference_id,
52
+ expires_in: config[:code_expires_in]
48
53
  )
49
54
  OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], code: code, state: query["state"], iss: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)))
50
55
  end
@@ -7,9 +7,11 @@ module BetterAuth
7
7
  def oauth_introspect_endpoint(config)
8
8
  Endpoint.new(path: "/oauth2/introspect", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
9
9
  client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
10
+ client_id = OAuthProtocol.stringify_keys(client)["clientId"]
10
11
  body = OAuthProtocol.stringify_keys(ctx.body)
11
- token = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix])
12
- active = token && !token["revoked"] && (!token["expiresAt"] || token["expiresAt"] > Time.now)
12
+ token_value = body["token"].to_s.sub(/\ABearer\s+/i, "")
13
+ token = OAuthProtocol.find_token_by_hint(config[:store], token_value, body["token_type_hint"], prefix: config[:prefix])
14
+ active = token && token["clientId"].to_s == client_id.to_s && !token["revoked"] && (!token["expiresAt"] || token["expiresAt"] > Time.now)
13
15
  if active
14
16
  next ctx.json({
15
17
  active: true,
@@ -24,7 +26,7 @@ module BetterAuth
24
26
  })
25
27
  end
26
28
 
27
- jwt = oauth_introspect_jwt_access_token(ctx, client, body["token"].to_s)
29
+ jwt = oauth_introspect_jwt_access_token(ctx, client, token_value)
28
30
  ctx.json(jwt || {active: false})
29
31
  end
30
32
  end
@@ -34,7 +36,7 @@ module BetterAuth
34
36
  end
35
37
 
36
38
  def oauth_introspect_jwt_access_token(ctx, client, token)
37
- payload = ::JWT.decode(token, ctx.context.secret, true, algorithm: "HS256").first
39
+ payload = OAuthProtocol.verify_oauth_jwt(ctx, token, issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)), hs256_secret: ctx.context.secret)
38
40
  client_data = OAuthProtocol.stringify_keys(client)
39
41
  return nil unless payload["azp"] == client_data["clientId"]
40
42
 
@@ -19,7 +19,12 @@ module BetterAuth
19
19
  raise APIError.new("BAD_REQUEST", message: "invalid_client") if client_data["disabled"]
20
20
  raise APIError.new("UNAUTHORIZED", message: "client unable to logout") unless client_data["enableEndSession"]
21
21
 
22
- payload = Crypto.verify_jwt(id_token_hint, client_data["clientId"])
22
+ payload = OAuthProtocol.verify_oauth_jwt(
23
+ ctx,
24
+ id_token_hint,
25
+ issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)),
26
+ hs256_secret: OAuthProtocol.id_token_hs256_key(ctx, client_data["clientId"], client_data["clientSecret"])
27
+ )
23
28
  raise APIError.new("UNAUTHORIZED", message: "invalid id token") unless payload
24
29
  raise APIError.new("BAD_REQUEST", message: "audience mismatch") if input["client_id"] && payload["aud"] != input["client_id"]
25
30
 
@@ -11,7 +11,6 @@ module BetterAuth
11
11
  issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)),
12
12
  authorization_endpoint: "#{base}/oauth2/authorize",
13
13
  token_endpoint: "#{base}/oauth2/token",
14
- registration_endpoint: "#{base}/oauth2/register",
15
14
  introspection_endpoint: "#{base}/oauth2/introspect",
16
15
  revocation_endpoint: "#{base}/oauth2/revoke",
17
16
  response_types_supported: ["code"],
@@ -24,6 +23,7 @@ module BetterAuth
24
23
  authorization_response_iss_parameter_supported: true,
25
24
  scopes_supported: config.dig(:advertised_metadata, :scopes_supported) || config[:scopes]
26
25
  }
26
+ metadata[:registration_endpoint] = "#{base}/oauth2/register" if config[:allow_dynamic_client_registration]
27
27
  metadata[:jwks_uri] = oauth_jwks_uri(config) if oauth_jwks_uri(config)
28
28
  ctx.json(metadata, headers: oauth_metadata_headers)
29
29
  end
@@ -40,7 +40,6 @@ module BetterAuth
40
40
  issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)),
41
41
  authorization_endpoint: "#{base}/oauth2/authorize",
42
42
  token_endpoint: "#{base}/oauth2/token",
43
- registration_endpoint: "#{base}/oauth2/register",
44
43
  introspection_endpoint: "#{base}/oauth2/introspect",
45
44
  revocation_endpoint: "#{base}/oauth2/revoke",
46
45
  response_types_supported: ["code"],
@@ -60,6 +59,7 @@ module BetterAuth
60
59
  prompt_values_supported: oauth_prompt_values,
61
60
  claims_supported: config.dig(:advertised_metadata, :claims_supported) || config[:claims] || []
62
61
  }
62
+ metadata[:registration_endpoint] = "#{base}/oauth2/register" if config[:allow_dynamic_client_registration]
63
63
  metadata[:jwks_uri] = oauth_jwks_uri(config) if oauth_jwks_uri(config)
64
64
  ctx.json(metadata, headers: oauth_metadata_headers)
65
65
  end
@@ -85,6 +85,8 @@ module BetterAuth
85
85
  return ["HS256"] if config[:disable_jwt_plugin]
86
86
 
87
87
  jwt_plugin = ctx.context.options.plugins.find { |plugin| plugin.id == "jwt" }
88
+ return ["HS256"] unless jwt_plugin
89
+
88
90
  alg = config.dig(:jwt, :jwks, :key_pair_config, :alg) ||
89
91
  jwt_plugin&.options&.dig(:jwks, :key_pair_config, :alg)
90
92
  alg ? [alg] : ["EdDSA"]
@@ -53,9 +53,16 @@ module BetterAuth
53
53
  end
54
54
  end
55
55
 
56
- def oauth_get_client_public_prelogin_endpoint(_config)
56
+ def oauth_get_client_public_prelogin_endpoint(config)
57
57
  Endpoint.new(path: "/oauth2/public-client-prelogin", method: "POST") do |ctx|
58
58
  input = OAuthProtocol.stringify_keys(ctx.body).merge(OAuthProtocol.stringify_keys(ctx.query))
59
+ unless config[:allow_public_client_prelogin] || config[:allowPublicClientPrelogin]
60
+ raise APIError.new("BAD_REQUEST")
61
+ end
62
+ unless OAuthProvider::Utils.verify_oauth_query_params(input["oauth_query"], ctx.context.secret)
63
+ raise APIError.new("UNAUTHORIZED", body: {error: "invalid_signature"})
64
+ end
65
+
59
66
  client = OAuthProtocol.find_client(ctx, "oauthClient", input["client_id"])
60
67
  raise APIError.new("NOT_FOUND", message: "client not found") unless client
61
68
  raise APIError.new("NOT_FOUND", message: "client not found") if OAuthProtocol.stringify_keys(client)["disabled"]
@@ -101,6 +108,7 @@ module BetterAuth
101
108
  oauth_assert_owned_client!(client, session, config)
102
109
 
103
110
  update_source = OAuthProtocol.stringify_keys(body["update"] || {})
111
+ oauth_validate_client_update!(client, update_source, config, admin: false)
104
112
  update = oauth_client_update_data(update_source)
105
113
  updated = update.empty? ? client : ctx.context.adapter.update(model: "oauthClient", where: [{field: "clientId", value: body["client_id"]}], update: update.merge(updatedAt: Time.now))
106
114
  ctx.json(OAuthProtocol.client_response(updated, include_secret: false))
@@ -136,13 +144,15 @@ module BetterAuth
136
144
  end
137
145
  end
138
146
 
139
- def oauth_admin_update_client_endpoint(_config)
147
+ def oauth_admin_update_client_endpoint(config)
140
148
  Endpoint.new(path: "/admin/oauth2/update-client", method: "PATCH", metadata: {server_only: true}) do |ctx|
141
149
  body = OAuthProtocol.stringify_keys(ctx.body)
142
150
  client = OAuthProtocol.find_client(ctx, "oauthClient", body["client_id"])
143
151
  raise APIError.new("NOT_FOUND", message: "client not found") unless client
144
152
 
145
- update = oauth_client_update_data(OAuthProtocol.stringify_keys(body["update"] || {}), admin: true)
153
+ update_source = OAuthProtocol.stringify_keys(body["update"] || {})
154
+ oauth_validate_client_update!(client, update_source, config, admin: true)
155
+ update = oauth_client_update_data(update_source, admin: true)
146
156
  updated = update.empty? ? client : ctx.context.adapter.update(model: "oauthClient", where: [{field: "clientId", value: body["client_id"]}], update: update.merge(updatedAt: Time.now))
147
157
  ctx.json(OAuthProtocol.client_response(updated, include_secret: false))
148
158
  end
@@ -46,9 +46,12 @@ module BetterAuth
46
46
  update["redirectUrls"] = redirects.join(",")
47
47
  end
48
48
  update["postLogoutRedirectUris"] = Array(source["post_logout_redirect_uris"]).map(&:to_s) if source.key?("post_logout_redirect_uris")
49
+ update["tokenEndpointAuthMethod"] = source["token_endpoint_auth_method"] || source["tokenEndpointAuthMethod"] if admin && (source.key?("token_endpoint_auth_method") || source.key?("tokenEndpointAuthMethod"))
49
50
  update["grantTypes"] = Array(source["grant_types"]).map(&:to_s) if source.key?("grant_types")
50
51
  update["responseTypes"] = Array(source["response_types"]).map(&:to_s) if source.key?("response_types")
51
52
  update["scopes"] = OAuthProtocol.parse_scopes(source["scope"] || source["scopes"]) if source.key?("scope") || source.key?("scopes")
53
+ update["type"] = source["type"] if admin && source.key?("type")
54
+ update["public"] = !!source["public"] if admin && source.key?("public")
52
55
  update["enableEndSession"] = !!(source["enable_end_session"] || source["enableEndSession"]) if source.key?("enable_end_session") || source.key?("enableEndSession")
53
56
  update["skipConsent"] = !!(source["skip_consent"] || source["skipConsent"]) if admin && (source.key?("skip_consent") || source.key?("skipConsent"))
54
57
  update["clientSecretExpiresAt"] = source["client_secret_expires_at"] if admin && source.key?("client_secret_expires_at")
@@ -57,6 +60,41 @@ module BetterAuth
57
60
  update
58
61
  end
59
62
 
63
+ def oauth_validate_client_update!(client, source, config, admin:)
64
+ return if source.empty?
65
+
66
+ current = OAuthProtocol.stringify_keys(client)
67
+ source = source.except("public", "token_endpoint_auth_method", "tokenEndpointAuthMethod", "client_secret", "clientSecret", "type") unless admin
68
+ return if source.empty?
69
+
70
+ redirects = source.key?("redirect_uris") ? Array(source["redirect_uris"]).map(&:to_s) : OAuthProtocol.client_redirect_uris(current)
71
+ redirects.each { |uri| OAuthProtocol.validate_safe_url!(uri, field: "redirect_uris") }
72
+ if source.key?("post_logout_redirect_uris")
73
+ Array(source["post_logout_redirect_uris"]).map(&:to_s).each { |uri| OAuthProtocol.validate_safe_url!(uri, field: "post_logout_redirect_uris") }
74
+ end
75
+
76
+ auth_method = source["token_endpoint_auth_method"] || source["tokenEndpointAuthMethod"] || current["tokenEndpointAuthMethod"] || "client_secret_basic"
77
+ body = {
78
+ "token_endpoint_auth_method" => auth_method,
79
+ "grant_types" => source.key?("grant_types") ? Array(source["grant_types"]).map(&:to_s) : Array(current["grantTypes"]).map(&:to_s),
80
+ "response_types" => source.key?("response_types") ? Array(source["response_types"]).map(&:to_s) : Array(current["responseTypes"]).map(&:to_s),
81
+ "type" => source.key?("type") ? source["type"] : current["type"],
82
+ "subject_type" => source["subject_type"] || source["subjectType"] || current["subjectType"]
83
+ }.compact
84
+ OAuthProtocol.validate_client_metadata_enums!(auth_method, body)
85
+ OAuthProtocol.validate_admin_only_fields!(source, admin: admin)
86
+ OAuthProtocol.validate_client_registration!(auth_method, body["grant_types"], body["response_types"], body, unauthenticated: false, dynamic_registration: false)
87
+ OAuthProtocol.validate_pairwise_client!(body, redirects, config[:pairwise_secret])
88
+
89
+ return unless source.key?("scope") || source.key?("scopes")
90
+
91
+ scopes = OAuthProtocol.parse_scopes(source["scope"] || source["scopes"])
92
+ allowed = OAuthProtocol.parse_scopes(config[:client_registration_allowed_scopes] || config[:scopes])
93
+ unless allowed.empty? || scopes.all? { |scope| allowed.include?(scope) }
94
+ raise APIError.new("BAD_REQUEST", message: "invalid_scope")
95
+ end
96
+ end
97
+
60
98
  def oauth_public_client_response(client)
61
99
  data = OAuthProtocol.stringify_keys(client)
62
100
  {
@@ -12,7 +12,10 @@ module BetterAuth
12
12
  oauth_rate_limit_rule(rate_limit, :introspect, "/oauth2/introspect", window: 60, max: 100),
13
13
  oauth_rate_limit_rule(rate_limit, :revoke, "/oauth2/revoke", window: 60, max: 30),
14
14
  oauth_rate_limit_rule(rate_limit, :register, "/oauth2/register", window: 60, max: 5),
15
- oauth_rate_limit_rule(rate_limit, :userinfo, "/oauth2/userinfo", window: 60, max: 60)
15
+ oauth_rate_limit_rule(rate_limit, :userinfo, "/oauth2/userinfo", window: 60, max: 60),
16
+ oauth_rate_limit_rule(rate_limit, :continue, "/oauth2/continue", window: 60, max: 40),
17
+ oauth_rate_limit_rule(rate_limit, :consent, "/oauth2/consent", window: 60, max: 40),
18
+ oauth_rate_limit_rule(rate_limit, :end_session, "/oauth2/end-session", window: 60, max: 30)
16
19
  ].compact
17
20
  end
18
21
 
@@ -6,7 +6,8 @@ module BetterAuth
6
6
 
7
7
  def oauth_revoke_endpoint(config)
8
8
  Endpoint.new(path: "/oauth2/revoke", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
9
- OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
9
+ client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
10
+ client_id = OAuthProtocol.stringify_keys(client)["clientId"]
10
11
  body = OAuthProtocol.stringify_keys(ctx.body)
11
12
  if body["token_type_hint"].to_s == "access_token" && OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, "refresh_token", prefix: config[:prefix])
12
13
  raise APIError.new("BAD_REQUEST", message: "invalid_request")
@@ -14,11 +15,31 @@ module BetterAuth
14
15
  if body["token_type_hint"].to_s == "refresh_token" && OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, "access_token", prefix: config[:prefix])
15
16
  raise APIError.new("BAD_REQUEST", message: "invalid_request")
16
17
  end
17
- if (token = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix]))
18
+ if (token = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix])) && token["clientId"].to_s == client_id.to_s
18
19
  token["revoked"] = Time.now
20
+ oauth_persist_token_revocation(ctx, config, body, token)
19
21
  end
20
22
  ctx.json({revoked: true})
21
23
  end
22
24
  end
25
+
26
+ def oauth_persist_token_revocation(ctx, config, body, token)
27
+ return unless token["id"]
28
+
29
+ hint = body["token_type_hint"].to_s
30
+ token_value = body["token"].to_s
31
+ access_value = OAuthProtocol.strip_prefix(token_value, config[:prefix], :access_token)
32
+ refresh_value = OAuthProtocol.strip_prefix(token_value, config[:prefix], :refresh_token)
33
+ is_access = hint == "access_token" || (access_value && config[:store][:tokens][access_value].equal?(token))
34
+ is_refresh = hint == "refresh_token" || (refresh_value && config[:store][:refresh_tokens][refresh_value].equal?(token))
35
+
36
+ if is_access && OAuthProtocol.schema_model?(ctx, "oauthAccessToken")
37
+ ctx.context.adapter.update(model: "oauthAccessToken", where: [{field: "id", value: token["id"]}], update: {revoked: token["revoked"]})
38
+ end
39
+
40
+ if is_refresh && OAuthProtocol.schema_model?(ctx, "oauthRefreshToken")
41
+ ctx.context.adapter.update(model: "oauthRefreshToken", where: [{field: "id", value: token["id"]}], update: {revoked: token["revoked"]})
42
+ end
43
+ end
23
44
  end
24
45
  end
@@ -12,8 +12,6 @@ module BetterAuth
12
12
  if client_grants.any? && !client_grants.include?(body["grant_type"].to_s)
13
13
  raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type")
14
14
  end
15
- audience = oauth_validate_resource!(ctx, config, body)
16
-
17
15
  response = case body["grant_type"]
18
16
  when OAuthProtocol::AUTH_CODE_GRANT
19
17
  code = OAuthProtocol.consume_code!(
@@ -23,6 +21,7 @@ module BetterAuth
23
21
  redirect_uri: body["redirect_uri"],
24
22
  code_verifier: body["code_verifier"]
25
23
  )
24
+ audience = oauth_validate_resource!(ctx, config, body, code[:scopes])
26
25
  OAuthProtocol.issue_tokens(
27
26
  ctx,
28
27
  config[:store],
@@ -35,12 +34,14 @@ module BetterAuth
35
34
  prefix: config[:prefix],
36
35
  refresh_token_expires_in: config[:refresh_token_expires_in],
37
36
  access_token_expires_in: oauth_access_token_expires_in(config, code[:scopes], machine: false),
37
+ id_token_expires_in: config[:id_token_expires_in],
38
38
  audience: audience,
39
39
  grant_type: OAuthProtocol::AUTH_CODE_GRANT,
40
40
  custom_token_response_fields: config[:custom_token_response_fields],
41
41
  custom_access_token_claims: config[:custom_access_token_claims],
42
42
  custom_id_token_claims: config[:custom_id_token_claims],
43
43
  jwt_access_token: oauth_jwt_access_token?(config, audience),
44
+ use_jwt_plugin: !config[:disable_jwt_plugin],
44
45
  pairwise_secret: config[:pairwise_secret],
45
46
  nonce: code[:nonce],
46
47
  auth_time: code[:auth_time],
@@ -49,17 +50,28 @@ module BetterAuth
49
50
  )
50
51
  when OAuthProtocol::CLIENT_CREDENTIALS_GRANT
51
52
  requested = OAuthProtocol.parse_scopes(body["scope"])
52
- allowed = OAuthProtocol.parse_scopes(OAuthProtocol.stringify_keys(client)["scopes"] || config[:scopes])
53
+ oidc_scopes = %w[openid profile email offline_access]
54
+ unless (requested & oidc_scopes).empty?
55
+ raise APIError.new("BAD_REQUEST", message: "invalid_scope")
56
+ end
57
+ client_data = OAuthProtocol.stringify_keys(client)
58
+ allowed = if client_data.key?("scopes") && !client_data["scopes"].nil?
59
+ OAuthProtocol.parse_scopes(client_data["scopes"])
60
+ else
61
+ OAuthProtocol.parse_scopes(config[:client_credential_grant_default_scopes] || config[:scopes])
62
+ end
53
63
  requested = allowed if requested.empty?
54
64
  unless requested.all? { |scope| allowed.include?(scope) }
55
65
  raise APIError.new("BAD_REQUEST", message: "invalid_scope")
56
66
  end
57
67
 
58
- OAuthProtocol.issue_tokens(ctx, config[:store], model: "oauthAccessToken", client: client, session: {"user" => {}, "session" => {}}, scopes: requested, include_refresh: false, issuer: OAuthProtocol.issuer(ctx), prefix: config[:prefix], audience: audience, grant_type: OAuthProtocol::CLIENT_CREDENTIALS_GRANT, custom_token_response_fields: config[:custom_token_response_fields], custom_access_token_claims: config[:custom_access_token_claims], custom_id_token_claims: config[:custom_id_token_claims], jwt_access_token: oauth_jwt_access_token?(config, audience), pairwise_secret: config[:pairwise_secret], access_token_expires_in: oauth_access_token_expires_in(config, requested, machine: true), filter_id_token_claims_by_scope: true)
68
+ audience = oauth_validate_resource!(ctx, config, body, requested)
69
+ OAuthProtocol.issue_tokens(ctx, config[:store], model: "oauthAccessToken", client: client, session: {"user" => {}, "session" => {}}, scopes: requested, include_refresh: false, issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)), prefix: config[:prefix], audience: audience, grant_type: OAuthProtocol::CLIENT_CREDENTIALS_GRANT, custom_token_response_fields: config[:custom_token_response_fields], custom_access_token_claims: config[:custom_access_token_claims], custom_id_token_claims: config[:custom_id_token_claims], jwt_access_token: oauth_jwt_access_token?(config, audience), use_jwt_plugin: !config[:disable_jwt_plugin], pairwise_secret: config[:pairwise_secret], access_token_expires_in: oauth_access_token_expires_in(config, requested, machine: true), id_token_expires_in: config[:id_token_expires_in], filter_id_token_claims_by_scope: true)
59
70
  when OAuthProtocol::REFRESH_GRANT
60
71
  refresh_record = OAuthProtocol.find_token_by_hint(config[:store], body["refresh_token"].to_s, "refresh_token", prefix: config[:prefix])
61
72
  refresh_scopes = OAuthProtocol.parse_scopes(body["scope"] || refresh_record&.fetch("scopes", nil))
62
- OAuthProtocol.refresh_tokens(ctx, config[:store], model: "oauthAccessToken", client: client, refresh_token: body["refresh_token"], scopes: body["scope"], issuer: OAuthProtocol.issuer(ctx), prefix: config[:prefix], refresh_token_expires_in: config[:refresh_token_expires_in], audience: audience, custom_token_response_fields: config[:custom_token_response_fields], custom_access_token_claims: config[:custom_access_token_claims], custom_id_token_claims: config[:custom_id_token_claims], jwt_access_token: oauth_jwt_access_token?(config, audience), pairwise_secret: config[:pairwise_secret], access_token_expires_in: oauth_access_token_expires_in(config, refresh_scopes, machine: false), filter_id_token_claims_by_scope: true)
73
+ audience = oauth_validate_resource!(ctx, config, body, refresh_scopes)
74
+ OAuthProtocol.refresh_tokens(ctx, config[:store], model: "oauthAccessToken", client: client, refresh_token: body["refresh_token"], scopes: body["scope"], issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)), prefix: config[:prefix], refresh_token_expires_in: config[:refresh_token_expires_in], audience: audience, custom_token_response_fields: config[:custom_token_response_fields], custom_access_token_claims: config[:custom_access_token_claims], custom_id_token_claims: config[:custom_id_token_claims], jwt_access_token: oauth_jwt_access_token?(config, audience), use_jwt_plugin: !config[:disable_jwt_plugin], pairwise_secret: config[:pairwise_secret], access_token_expires_in: oauth_access_token_expires_in(config, refresh_scopes, machine: false), id_token_expires_in: config[:id_token_expires_in], filter_id_token_claims_by_scope: true)
63
75
  else
64
76
  raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type")
65
77
  end
@@ -67,17 +79,21 @@ module BetterAuth
67
79
  end
68
80
  end
69
81
 
70
- def oauth_validate_resource!(ctx, config, body)
82
+ def oauth_validate_resource!(ctx, config, body, scopes)
71
83
  resources = Array(body["resource"]).compact.map(&:to_s)
72
84
  return nil if resources.empty?
73
85
 
86
+ userinfo_audience = "#{OAuthProtocol.endpoint_base(ctx)}/oauth2/userinfo"
87
+ requested = resources.dup
88
+ requested << userinfo_audience if OAuthProtocol.parse_scopes(scopes).include?("openid") && !requested.include?(userinfo_audience)
74
89
  valid = Array(config[:valid_audiences]).map(&:to_s)
75
- return (resources.length == 1) ? resources.first : resources if valid.empty?
90
+ valid = [OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx))] if valid.empty?
91
+ valid << userinfo_audience if OAuthProtocol.parse_scopes(scopes).include?("openid") && !valid.include?(userinfo_audience)
76
92
 
77
- resources.each do |resource|
93
+ requested.each do |resource|
78
94
  raise APIError.new("BAD_REQUEST", message: "requested resource invalid") unless valid.include?(resource)
79
95
  end
80
- (resources.length == 1) ? resources.first : resources
96
+ (requested.length == 1) ? requested.first : requested
81
97
  end
82
98
 
83
99
  def oauth_access_token_expires_in(config, scopes, machine:)
@@ -6,7 +6,7 @@ module BetterAuth
6
6
 
7
7
  def oauth_userinfo_endpoint(config)
8
8
  Endpoint.new(path: "/oauth2/userinfo", method: "GET") do |ctx|
9
- ctx.json(OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], additional_claim: config[:custom_user_info_claims] || config[:additional_claim], prefix: config[:prefix], jwt_secret: ctx.context.secret))
9
+ ctx.json(OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], additional_claim: config[:custom_user_info_claims] || config[:additional_claim], prefix: config[:prefix], jwt_secret: ctx.context.secret, ctx: ctx, issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx))))
10
10
  end
11
11
  end
12
12
  end
@@ -50,9 +50,12 @@ module BetterAuth
50
50
  post_login: {},
51
51
  store_client_secret: "plain",
52
52
  prefix: {},
53
+ code_expires_in: 600,
54
+ id_token_expires_in: 36_000,
53
55
  refresh_token_expires_in: 2_592_000,
54
56
  access_token_expires_in: 3600,
55
57
  m2m_access_token_expires_in: 3600,
58
+ client_credential_grant_default_scopes: nil,
56
59
  scope_expirations: {},
57
60
  store: OAuthProtocol.stores
58
61
  }.merge(normalize_hash(options))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-oauth-provider
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala