better_auth-oauth-provider 0.8.0 → 0.10.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 +8 -0
- data/README.md +18 -1
- data/lib/better_auth/oauth_provider/version.rb +1 -1
- data/lib/better_auth/plugins/oauth_provider/authorize.rb +22 -2
- data/lib/better_auth/plugins/oauth_provider/consent.rb +3 -2
- data/lib/better_auth/plugins/oauth_provider/continue.rb +1 -1
- data/lib/better_auth/plugins/oauth_provider/introspect.rb +2 -2
- data/lib/better_auth/plugins/oauth_provider/logout.rb +1 -1
- data/lib/better_auth/plugins/oauth_provider/mcp.rb +2 -0
- data/lib/better_auth/plugins/oauth_provider/metadata.rb +17 -4
- data/lib/better_auth/plugins/oauth_provider/oauth_client/endpoints.rb +11 -6
- data/lib/better_auth/plugins/oauth_provider/oauth_consent/endpoints.rb +3 -3
- data/lib/better_auth/plugins/oauth_provider/register.rb +6 -1
- data/lib/better_auth/plugins/oauth_provider/revoke.rb +15 -2
- data/lib/better_auth/plugins/oauth_provider/schema.rb +16 -16
- data/lib/better_auth/plugins/oauth_provider/token.rb +38 -10
- data/lib/better_auth/plugins/oauth_provider.rb +516 -2
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0afe75d53fedd5aca49d956067e9c692c12422467b30c2344ed421bcbeb3ac50
|
|
4
|
+
data.tar.gz: dd9b3bfb6ee900376a1c98cf42ddc4db4044bd0a8838b8880dfe51e7d6799ee0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cd8ed966b7207a02dc8fad76f6e231084c43cee1e1fed5381d5bf1280b6b36e603220c848e74046ea6f6f2bb0a26ef94bb890bfed4a540649e6ac1642577be2
|
|
7
|
+
data.tar.gz: 8441b20dc28394bf3294d0527ff7818661cba1f485b83c218cdcf25a312cc0ac94d0b3825017305ad88b48d4131a4e867fb4f76d2d3bede870678f9361676b22
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.10.0 - 2026-05-21
|
|
6
|
+
|
|
7
|
+
- Changed OAuth provider defaults to hash stored client secrets and opaque OAuth tokens, with `store_tokens` support for custom token hashing.
|
|
8
|
+
- Hardened token, introspection, and revocation client authentication to enforce the registered auth method and reject public-client introspection/revocation.
|
|
9
|
+
- Aligned refresh-token issuance with upstream by requiring `offline_access`, and revoking descendant access tokens when a refresh token is revoked.
|
|
10
|
+
- Added no-store token response headers, default JWKS discovery metadata, authorization-code session revalidation, prompt/request-uri validation, MCP verifier error normalization, and OAuth hot-path schema indexes.
|
|
11
|
+
- Expanded adapter, authorization, registration, and rate-limit coverage for provider flows.
|
|
12
|
+
|
|
5
13
|
## 0.7.0 - 2026-05-05
|
|
6
14
|
|
|
7
15
|
- Fixed OAuth provider consent approval, metadata, issuer normalization, revocation persistence, and endpoint-specific rate limits for hardening parity.
|
data/README.md
CHANGED
|
@@ -108,6 +108,7 @@ Common options accepted by `BetterAuth::Plugins.oauth_provider`:
|
|
|
108
108
|
- `client_registration_allowed_scopes`
|
|
109
109
|
- `client_credential_grant_default_scopes`
|
|
110
110
|
- `store_client_secret`
|
|
111
|
+
- `store_tokens`
|
|
111
112
|
- `prefix`
|
|
112
113
|
- `code_expires_in`
|
|
113
114
|
- `id_token_expires_in`
|
|
@@ -131,6 +132,10 @@ Common options accepted by `BetterAuth::Plugins.oauth_provider`:
|
|
|
131
132
|
- `disable_jwt_plugin`
|
|
132
133
|
- `store`
|
|
133
134
|
|
|
135
|
+
`store_client_secret` defaults to `"hashed"`. Set `store_client_secret: "plain"` only when migrating an existing app that still depends on plaintext client secrets. `store_tokens` defaults to `"hashed"` for opaque access tokens, refresh tokens, and authorization codes; custom hash callbacks may be supplied with `hash: ->(token, type) { ... }`.
|
|
136
|
+
|
|
137
|
+
Token, introspection, and revocation client authentication is method-strict: `client_secret_basic` clients must authenticate with HTTP Basic credentials, `client_secret_post` clients must use body credentials, and public clients cannot authenticate to introspection or revocation. Authorization-code clients receive refresh tokens only when the granted scope includes `offline_access`.
|
|
138
|
+
|
|
134
139
|
`rate_limit` accepts per-route overrides:
|
|
135
140
|
|
|
136
141
|
```ruby
|
|
@@ -150,6 +155,8 @@ Use `false` to disable a route-specific rule.
|
|
|
150
155
|
|
|
151
156
|
`oauthAccessToken` now uses the upstream canonical columns `token`, `expiresAt`, `scopes`, `clientId`, `sessionId`, `userId`, `referenceId`, and `refreshId`. Legacy `access_token`, `refresh_token`, `access_token_expires_at`, and `scope` columns should be copied forward then dropped. `oauthConsent#consent_given` is also removed; a consent row means consent was granted.
|
|
152
157
|
|
|
158
|
+
After enabling the hashed defaults, newly created OAuth client secrets and opaque tokens are stored hashed. Existing plaintext rows continue to require either a migration that re-registers/rotates secrets and tokens or a temporary explicit `store_client_secret: "plain"` setting for old clients during rollout.
|
|
159
|
+
|
|
153
160
|
For non-Rails SQL apps, run the equivalent of:
|
|
154
161
|
|
|
155
162
|
```sql
|
|
@@ -166,7 +173,7 @@ Then drop the legacy access-token and consent columns. Rails apps using `better_
|
|
|
166
173
|
|
|
167
174
|
## Ruby Adaptations
|
|
168
175
|
|
|
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.
|
|
176
|
+
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, and discovery metadata publishes the active JWKS URI. If the JWT plugin is not registered, or `disable_jwt_plugin: true` is set, Ruby intentionally falls back to HS256 for compatibility; with hashed client-secret storage, that fallback uses a server-derived per-client key.
|
|
170
177
|
|
|
171
178
|
Upstream `oauthProviderResourceClient` and MCP protected-resource helpers remain future API-boundary work for Ruby. This gem currently hardens authorization-server behavior only.
|
|
172
179
|
|
|
@@ -175,3 +182,13 @@ Route OpenAPI metadata blocks from upstream TypeScript are intentionally not por
|
|
|
175
182
|
The upstream `@better-auth/oauth-provider/client`, React/Solid client plugins, dashboard UI, and browser helpers are not ported. Ruby apps call the JSON endpoints directly or wrap `auth.api.*`.
|
|
176
183
|
|
|
177
184
|
OIDC provider remains a core `better_auth` plugin because upstream still exposes it from `better-auth/plugins`. OAuth provider is the newer standalone provider package.
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
Run the package suite with:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
rbenv exec bundle exec rake test
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Adapter smoke tests skip when optional adapter gems or database services are not available. To install the optional adapter test bundle locally, enable Bundler's `adapter_test` group before running the suite.
|
|
@@ -40,6 +40,9 @@ module BetterAuth
|
|
|
40
40
|
scopes = OAuthProtocol.parse_scopes(query["scope"])
|
|
41
41
|
scopes = OAuthProtocol.parse_scopes(OAuthProtocol.stringify_keys(client)["scopes"] || config[:scopes]) if scopes.empty?
|
|
42
42
|
prompts = OAuthProtocol.parse_scopes(query["prompt"])
|
|
43
|
+
if prompts.include?("none") && (prompts - ["none"]).any?
|
|
44
|
+
raise ctx.redirect(oauth_authorize_error_redirect(ctx, query, "invalid_request", "prompt none cannot be combined with other prompts"))
|
|
45
|
+
end
|
|
43
46
|
client_data = OAuthProtocol.stringify_keys(client)
|
|
44
47
|
if client_data["disabled"]
|
|
45
48
|
raise ctx.redirect(oauth_authorize_error_redirect(ctx, query, "invalid_client", "client is disabled"))
|
|
@@ -65,7 +68,7 @@ module BetterAuth
|
|
|
65
68
|
raise ctx.redirect(oauth_prompt_redirect(ctx, config, query, "login"))
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
if
|
|
71
|
+
if oauth_requires_login?(session, prompts, query) && !continue_post_login
|
|
69
72
|
raise ctx.redirect(oauth_prompt_redirect(ctx, config, query, "login"))
|
|
70
73
|
end
|
|
71
74
|
|
|
@@ -111,6 +114,21 @@ module BetterAuth
|
|
|
111
114
|
oauth_redirect_with_code(ctx, config, query, session, client, scopes, reference_id: consent_reference_id)
|
|
112
115
|
end
|
|
113
116
|
|
|
117
|
+
def oauth_requires_login?(session, prompts, query)
|
|
118
|
+
return true if prompts.include?("login")
|
|
119
|
+
return false unless query.key?("max_age")
|
|
120
|
+
|
|
121
|
+
max_age = Integer(query["max_age"])
|
|
122
|
+
return false if max_age.negative?
|
|
123
|
+
|
|
124
|
+
auth_time = OAuthProvider::Utils.resolve_session_auth_time(session)
|
|
125
|
+
return false unless auth_time
|
|
126
|
+
|
|
127
|
+
(Time.now - auth_time) > max_age
|
|
128
|
+
rescue ArgumentError, TypeError
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
114
132
|
def oauth_prompt_redirect(ctx, config, query, type, page: nil)
|
|
115
133
|
target = page || oauth_prompt_page(config, type)
|
|
116
134
|
|
|
@@ -204,7 +222,9 @@ module BetterAuth
|
|
|
204
222
|
resolved = resolver.call({request_uri: query["request_uri"], client_id: query["client_id"], context: ctx})
|
|
205
223
|
return oauth_invalid_request_uri!(ctx, query, "request_uri is invalid or expired") unless resolved
|
|
206
224
|
|
|
207
|
-
OAuthProtocol.stringify_keys(resolved)
|
|
225
|
+
resolved_query = OAuthProtocol.stringify_keys(resolved)
|
|
226
|
+
resolved_query["client_id"] = query["client_id"] if query["client_id"]
|
|
227
|
+
resolved_query
|
|
208
228
|
end
|
|
209
229
|
|
|
210
230
|
def oauth_invalid_request_uri!(ctx, query, description)
|
|
@@ -5,7 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_consent_endpoint(config)
|
|
8
|
-
Endpoint.new(path: "/oauth2/consent", method: "POST") do |ctx|
|
|
8
|
+
Endpoint.new(path: "/oauth2/consent", method: "POST", metadata: oauth_openapi_for(:consent)) do |ctx|
|
|
9
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)
|
|
@@ -49,7 +49,8 @@ module BetterAuth
|
|
|
49
49
|
code_challenge_method: query["code_challenge_method"],
|
|
50
50
|
nonce: query["nonce"],
|
|
51
51
|
reference_id: reference_id || client_reference_id,
|
|
52
|
-
expires_in: config[:code_expires_in]
|
|
52
|
+
expires_in: config[:code_expires_in],
|
|
53
|
+
store_tokens: config[:store_tokens]
|
|
53
54
|
)
|
|
54
55
|
OAuthProtocol.redirect_uri_with_params(query["redirect_uri"], code: code, state: query["state"], iss: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)))
|
|
55
56
|
end
|
|
@@ -5,7 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_continue_endpoint(config)
|
|
8
|
-
Endpoint.new(path: "/oauth2/continue", method: "POST") do |ctx|
|
|
8
|
+
Endpoint.new(path: "/oauth2/continue", method: "POST", metadata: oauth_openapi_for(:continue)) do |ctx|
|
|
9
9
|
Routes.current_session(ctx)
|
|
10
10
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
11
11
|
action = if body["selected"] == true
|
|
@@ -5,8 +5,8 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_introspect_endpoint(config)
|
|
8
|
-
Endpoint.new(path: "/oauth2/introspect", method: "POST", metadata:
|
|
9
|
-
client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
|
|
8
|
+
Endpoint.new(path: "/oauth2/introspect", method: "POST", metadata: oauth_openapi_for(:introspect).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])) do |ctx|
|
|
9
|
+
client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix], require_confidential: true)
|
|
10
10
|
client_id = OAuthProtocol.stringify_keys(client)["clientId"]
|
|
11
11
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
12
12
|
token_value = body["token"].to_s.sub(/\ABearer\s+/i, "")
|
|
@@ -5,7 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_end_session_endpoint
|
|
8
|
-
Endpoint.new(path: "/oauth2/end-session", method: ["GET", "POST"], metadata:
|
|
8
|
+
Endpoint.new(path: "/oauth2/end-session", method: ["GET", "POST"], metadata: oauth_openapi_for(:end_session).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])) do |ctx|
|
|
9
9
|
input = OAuthProtocol.stringify_keys((ctx.method == "GET") ? ctx.query : ctx.body)
|
|
10
10
|
id_token_hint = input["id_token_hint"].to_s
|
|
11
11
|
raise APIError.new("UNAUTHORIZED", message: "invalid id token") if id_token_hint.empty?
|
|
@@ -58,6 +58,8 @@ module BetterAuth
|
|
|
58
58
|
handler.call(request, jwt)
|
|
59
59
|
rescue APIError => error
|
|
60
60
|
handle_mcp_errors(error, resource, resource_metadata_mappings: resource_metadata_mappings)
|
|
61
|
+
rescue ::JWT::DecodeError
|
|
62
|
+
handle_mcp_errors(APIError.new("UNAUTHORIZED", message: "invalid token"), resource, resource_metadata_mappings: resource_metadata_mappings)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
end
|
|
@@ -24,7 +24,8 @@ module BetterAuth
|
|
|
24
24
|
scopes_supported: config.dig(:advertised_metadata, :scopes_supported) || config[:scopes]
|
|
25
25
|
}
|
|
26
26
|
metadata[:registration_endpoint] = "#{base}/oauth2/register" if config[:allow_dynamic_client_registration]
|
|
27
|
-
|
|
27
|
+
jwks_uri = oauth_jwks_uri(ctx, config)
|
|
28
|
+
metadata[:jwks_uri] = jwks_uri if jwks_uri
|
|
28
29
|
ctx.json(metadata, headers: oauth_metadata_headers)
|
|
29
30
|
end
|
|
30
31
|
end
|
|
@@ -60,7 +61,8 @@ module BetterAuth
|
|
|
60
61
|
claims_supported: config.dig(:advertised_metadata, :claims_supported) || config[:claims] || []
|
|
61
62
|
}
|
|
62
63
|
metadata[:registration_endpoint] = "#{base}/oauth2/register" if config[:allow_dynamic_client_registration]
|
|
63
|
-
|
|
64
|
+
jwks_uri = oauth_jwks_uri(ctx, config)
|
|
65
|
+
metadata[:jwks_uri] = jwks_uri if jwks_uri
|
|
64
66
|
ctx.json(metadata, headers: oauth_metadata_headers)
|
|
65
67
|
end
|
|
66
68
|
end
|
|
@@ -69,10 +71,21 @@ module BetterAuth
|
|
|
69
71
|
{"Cache-Control" => "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400"}
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
def oauth_jwks_uri(config)
|
|
74
|
+
def oauth_jwks_uri(ctx, config)
|
|
73
75
|
config.dig(:advertised_metadata, :jwks_uri) ||
|
|
74
76
|
config[:jwks_uri] ||
|
|
75
|
-
config.dig(:jwks, :remote_url)
|
|
77
|
+
config.dig(:jwks, :remote_url) ||
|
|
78
|
+
oauth_default_jwks_uri(ctx, config)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def oauth_default_jwks_uri(ctx, config)
|
|
82
|
+
return nil if config[:disable_jwt_plugin]
|
|
83
|
+
|
|
84
|
+
jwt_plugin = ctx.context.options.plugins.find { |plugin| plugin.id == "jwt" }
|
|
85
|
+
return nil unless jwt_plugin
|
|
86
|
+
|
|
87
|
+
path = jwt_plugin.options&.dig(:jwks, :jwks_path) || "/jwks"
|
|
88
|
+
"#{OAuthProtocol.endpoint_base(ctx)}#{path}"
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
def oauth_token_auth_methods(config)
|
|
@@ -5,7 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_create_client_endpoint(config)
|
|
8
|
-
Endpoint.new(path: "/oauth2/create-client", method: "POST") do |ctx|
|
|
8
|
+
Endpoint.new(path: "/oauth2/create-client", method: "POST", metadata: oauth_openapi_for(:create_client)) do |ctx|
|
|
9
9
|
session = Routes.current_session(ctx)
|
|
10
10
|
oauth_assert_client_privilege!(ctx, config, session, "create")
|
|
11
11
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -54,7 +54,12 @@ module BetterAuth
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def oauth_get_client_public_prelogin_endpoint(config)
|
|
57
|
-
Endpoint.new(
|
|
57
|
+
Endpoint.new(
|
|
58
|
+
path: "/oauth2/public-client-prelogin",
|
|
59
|
+
method: "POST",
|
|
60
|
+
body_schema: ->(value) { value },
|
|
61
|
+
metadata: oauth_openapi_for(:public_client_prelogin)
|
|
62
|
+
) do |ctx|
|
|
58
63
|
input = OAuthProtocol.stringify_keys(ctx.body).merge(OAuthProtocol.stringify_keys(ctx.query))
|
|
59
64
|
unless config[:allow_public_client_prelogin] || config[:allowPublicClientPrelogin]
|
|
60
65
|
raise APIError.new("BAD_REQUEST")
|
|
@@ -86,7 +91,7 @@ module BetterAuth
|
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def oauth_delete_client_endpoint(config)
|
|
89
|
-
Endpoint.new(path: "/oauth2/delete-client", method: "POST") do |ctx|
|
|
94
|
+
Endpoint.new(path: "/oauth2/delete-client", method: "POST", metadata: oauth_openapi_for(:delete_client)) do |ctx|
|
|
90
95
|
session = Routes.current_session(ctx)
|
|
91
96
|
oauth_assert_client_privilege!(ctx, config, session, "delete")
|
|
92
97
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -99,7 +104,7 @@ module BetterAuth
|
|
|
99
104
|
end
|
|
100
105
|
|
|
101
106
|
def oauth_update_client_endpoint(config)
|
|
102
|
-
Endpoint.new(path: "/oauth2/update-client", method: "POST") do |ctx|
|
|
107
|
+
Endpoint.new(path: "/oauth2/update-client", method: "POST", metadata: oauth_openapi_for(:update_client)) do |ctx|
|
|
103
108
|
session = Routes.current_session(ctx)
|
|
104
109
|
oauth_assert_client_privilege!(ctx, config, session, "update")
|
|
105
110
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -159,7 +164,7 @@ module BetterAuth
|
|
|
159
164
|
end
|
|
160
165
|
|
|
161
166
|
def oauth_rotate_client_secret_endpoint(config)
|
|
162
|
-
Endpoint.new(path: "/oauth2/client/rotate-secret", method: "POST") do |ctx|
|
|
167
|
+
Endpoint.new(path: "/oauth2/client/rotate-secret", method: "POST", metadata: oauth_openapi_for(:rotate_client_secret)) do |ctx|
|
|
163
168
|
session = Routes.current_session(ctx)
|
|
164
169
|
oauth_assert_client_privilege!(ctx, config, session, "rotate")
|
|
165
170
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -210,7 +215,7 @@ module BetterAuth
|
|
|
210
215
|
end
|
|
211
216
|
|
|
212
217
|
def oauth_legacy_update_client_endpoint(config)
|
|
213
|
-
Endpoint.new(path: "/oauth2/client", method: "PATCH") do |ctx|
|
|
218
|
+
Endpoint.new(path: "/oauth2/client", method: "PATCH", metadata: oauth_openapi_for(:update_client)) do |ctx|
|
|
214
219
|
session = Routes.current_session(ctx)
|
|
215
220
|
oauth_assert_client_privilege!(ctx, config, session, "update")
|
|
216
221
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -30,7 +30,7 @@ module BetterAuth
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def oauth_update_consent_endpoint
|
|
33
|
-
Endpoint.new(path: "/oauth2/update-consent", method: "POST") do |ctx|
|
|
33
|
+
Endpoint.new(path: "/oauth2/update-consent", method: "POST", metadata: oauth_openapi_for(:update_consent)) do |ctx|
|
|
34
34
|
session = Routes.current_session(ctx)
|
|
35
35
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
36
36
|
id = body["id"]
|
|
@@ -61,7 +61,7 @@ module BetterAuth
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def oauth_delete_consent_endpoint
|
|
64
|
-
Endpoint.new(path: "/oauth2/delete-consent", method: "POST") do |ctx|
|
|
64
|
+
Endpoint.new(path: "/oauth2/delete-consent", method: "POST", metadata: oauth_openapi_for(:delete_consent)) do |ctx|
|
|
65
65
|
session = Routes.current_session(ctx)
|
|
66
66
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
67
67
|
id = body["id"]
|
|
@@ -98,7 +98,7 @@ module BetterAuth
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def oauth_legacy_update_consent_endpoint
|
|
101
|
-
Endpoint.new(path: "/oauth2/consent", method: "PATCH") do |ctx|
|
|
101
|
+
Endpoint.new(path: "/oauth2/consent", method: "PATCH", metadata: oauth_openapi_for(:update_consent)) do |ctx|
|
|
102
102
|
session = Routes.current_session(ctx)
|
|
103
103
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
104
104
|
consent = oauth_find_user_consent(ctx, session, body["client_id"])
|
|
@@ -5,7 +5,12 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_register_client_endpoint(config)
|
|
8
|
-
Endpoint.new(
|
|
8
|
+
Endpoint.new(
|
|
9
|
+
path: "/oauth2/register",
|
|
10
|
+
method: "POST",
|
|
11
|
+
body_schema: ->(value) { value },
|
|
12
|
+
metadata: oauth_openapi_for(:register_client)
|
|
13
|
+
) do |ctx|
|
|
9
14
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
10
15
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
11
16
|
unless config[:allow_dynamic_client_registration]
|
|
@@ -5,8 +5,8 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_revoke_endpoint(config)
|
|
8
|
-
Endpoint.new(path: "/oauth2/revoke", method: "POST", metadata:
|
|
9
|
-
client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
|
|
8
|
+
Endpoint.new(path: "/oauth2/revoke", method: "POST", metadata: oauth_openapi_for(:revoke).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])) do |ctx|
|
|
9
|
+
client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix], require_confidential: true)
|
|
10
10
|
client_id = OAuthProtocol.stringify_keys(client)["clientId"]
|
|
11
11
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
12
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])
|
|
@@ -39,7 +39,20 @@ module BetterAuth
|
|
|
39
39
|
|
|
40
40
|
if is_refresh && OAuthProtocol.schema_model?(ctx, "oauthRefreshToken")
|
|
41
41
|
ctx.context.adapter.update(model: "oauthRefreshToken", where: [{field: "id", value: token["id"]}], update: {revoked: token["revoked"]})
|
|
42
|
+
oauth_revoke_refresh_access_tokens(ctx, config[:store], token)
|
|
42
43
|
end
|
|
43
44
|
end
|
|
45
|
+
|
|
46
|
+
def oauth_revoke_refresh_access_tokens(ctx, store, refresh_token)
|
|
47
|
+
refresh_id = refresh_token["id"]
|
|
48
|
+
return if refresh_id.to_s.empty?
|
|
49
|
+
|
|
50
|
+
store[:tokens].each_value do |record|
|
|
51
|
+
record["revoked"] = refresh_token["revoked"] if record["refreshId"].to_s == refresh_id.to_s
|
|
52
|
+
end
|
|
53
|
+
return unless OAuthProtocol.schema_model?(ctx, "oauthAccessToken")
|
|
54
|
+
|
|
55
|
+
ctx.context.adapter.update_many(model: "oauthAccessToken", where: [{field: "refreshId", value: refresh_id}], update: {revoked: refresh_token["revoked"]})
|
|
56
|
+
end
|
|
44
57
|
end
|
|
45
58
|
end
|
|
@@ -7,7 +7,7 @@ module BetterAuth
|
|
|
7
7
|
def oauth_provider_schema
|
|
8
8
|
{
|
|
9
9
|
oauthClient: {
|
|
10
|
-
|
|
10
|
+
model_name: "oauth_clients",
|
|
11
11
|
fields: {
|
|
12
12
|
clientId: {type: "string", unique: true, required: true},
|
|
13
13
|
clientSecret: {type: "string", required: false},
|
|
@@ -16,7 +16,7 @@ module BetterAuth
|
|
|
16
16
|
enableEndSession: {type: "boolean", required: false},
|
|
17
17
|
clientSecretExpiresAt: {type: "number", required: false},
|
|
18
18
|
scopes: {type: "string[]", required: false},
|
|
19
|
-
userId: {type: "string", required: false},
|
|
19
|
+
userId: {type: "string", required: false, index: true, references: {model: "user", field: "id"}},
|
|
20
20
|
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
21
21
|
updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }},
|
|
22
22
|
name: {type: "string", required: false},
|
|
@@ -37,17 +37,17 @@ module BetterAuth
|
|
|
37
37
|
type: {type: "string", required: false},
|
|
38
38
|
requirePKCE: {type: "boolean", required: false},
|
|
39
39
|
subjectType: {type: "string", required: false},
|
|
40
|
-
referenceId: {type: "string", required: false},
|
|
40
|
+
referenceId: {type: "string", required: false, index: true},
|
|
41
41
|
metadata: {type: "json", required: false}
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
oauthRefreshToken: {
|
|
45
45
|
fields: {
|
|
46
46
|
token: {type: "string", required: true},
|
|
47
|
-
clientId: {type: "string", required: true},
|
|
48
|
-
sessionId: {type: "string", required: false},
|
|
49
|
-
userId: {type: "string", required: false},
|
|
50
|
-
referenceId: {type: "string", required: false},
|
|
47
|
+
clientId: {type: "string", required: true, index: true, references: {model: "oauthClient", field: "clientId"}},
|
|
48
|
+
sessionId: {type: "string", required: false, references: {model: "session", field: "id", on_delete: "set null"}},
|
|
49
|
+
userId: {type: "string", required: false, index: true, references: {model: "user", field: "id"}},
|
|
50
|
+
referenceId: {type: "string", required: false, index: true},
|
|
51
51
|
authTime: {type: "date", required: false},
|
|
52
52
|
expiresAt: {type: "date", required: false},
|
|
53
53
|
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
@@ -56,28 +56,28 @@ module BetterAuth
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
oauthAccessToken: {
|
|
59
|
-
|
|
59
|
+
model_name: "oauth_access_tokens",
|
|
60
60
|
fields: {
|
|
61
61
|
token: {type: "string", unique: true, required: true},
|
|
62
62
|
expiresAt: {type: "date", required: true},
|
|
63
|
-
clientId: {type: "string", required: true},
|
|
64
|
-
userId: {type: "string", required: false},
|
|
65
|
-
sessionId: {type: "string", required: false},
|
|
63
|
+
clientId: {type: "string", required: true, index: true, references: {model: "oauthClient", field: "clientId"}},
|
|
64
|
+
userId: {type: "string", required: false, index: true, references: {model: "user", field: "id"}},
|
|
65
|
+
sessionId: {type: "string", required: false, references: {model: "session", field: "id", on_delete: "set null"}},
|
|
66
66
|
scopes: {type: "string[]", required: true},
|
|
67
67
|
revoked: {type: "date", required: false},
|
|
68
68
|
referenceId: {type: "string", required: false},
|
|
69
69
|
authTime: {type: "date", required: false},
|
|
70
|
-
refreshId: {type: "string", required: false},
|
|
70
|
+
refreshId: {type: "string", required: false, index: true, references: {model: "oauthRefreshToken", field: "id"}},
|
|
71
71
|
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
72
72
|
updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }}
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
oauthConsent: {
|
|
76
|
-
|
|
76
|
+
model_name: "oauth_consents",
|
|
77
77
|
fields: {
|
|
78
|
-
clientId: {type: "string", required: true},
|
|
79
|
-
userId: {type: "string", required: false},
|
|
80
|
-
referenceId: {type: "string", required: false},
|
|
78
|
+
clientId: {type: "string", required: true, index: true, references: {model: "oauthClient", field: "clientId"}},
|
|
79
|
+
userId: {type: "string", required: false, index: true, references: {model: "user", field: "id"}},
|
|
80
|
+
referenceId: {type: "string", required: false, index: true},
|
|
81
81
|
scopes: {type: "string[]", required: true},
|
|
82
82
|
createdAt: {type: "date", required: true, default_value: -> { Time.now }},
|
|
83
83
|
updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }}
|
|
@@ -5,9 +5,15 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def oauth_token_endpoint(config)
|
|
8
|
-
Endpoint.new(
|
|
9
|
-
|
|
8
|
+
Endpoint.new(
|
|
9
|
+
path: "/oauth2/token",
|
|
10
|
+
method: "POST",
|
|
11
|
+
body_schema: ->(value) { value },
|
|
12
|
+
metadata: oauth_openapi_for(:token).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
13
|
+
) do |ctx|
|
|
14
|
+
body = OAuthProtocol.request_body!(ctx.body)
|
|
10
15
|
client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
|
|
16
|
+
client_id = OAuthProtocol.stringify_keys(client)["clientId"]
|
|
11
17
|
client_grants = OAuthProtocol.parse_scopes(OAuthProtocol.stringify_keys(client)["grantTypes"])
|
|
12
18
|
if client_grants.any? && !client_grants.include?(body["grant_type"].to_s)
|
|
13
19
|
raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type")
|
|
@@ -17,19 +23,21 @@ module BetterAuth
|
|
|
17
23
|
code = OAuthProtocol.consume_code!(
|
|
18
24
|
config[:store],
|
|
19
25
|
body["code"],
|
|
20
|
-
client_id:
|
|
26
|
+
client_id: client_id,
|
|
21
27
|
redirect_uri: body["redirect_uri"],
|
|
22
|
-
code_verifier: body["code_verifier"]
|
|
28
|
+
code_verifier: body["code_verifier"],
|
|
29
|
+
store_tokens: config[:store_tokens]
|
|
23
30
|
)
|
|
31
|
+
session = oauth_active_authorization_session!(ctx, code[:session])
|
|
24
32
|
audience = oauth_validate_resource!(ctx, config, body, code[:scopes])
|
|
25
33
|
OAuthProtocol.issue_tokens(
|
|
26
34
|
ctx,
|
|
27
35
|
config[:store],
|
|
28
36
|
model: "oauthAccessToken",
|
|
29
37
|
client: client,
|
|
30
|
-
session:
|
|
38
|
+
session: session,
|
|
31
39
|
scopes: code[:scopes],
|
|
32
|
-
include_refresh: code[:scopes].include?("offline_access")
|
|
40
|
+
include_refresh: code[:scopes].include?("offline_access"),
|
|
33
41
|
issuer: OAuthProvider.validate_issuer_url(OAuthProtocol.issuer(ctx)),
|
|
34
42
|
prefix: config[:prefix],
|
|
35
43
|
refresh_token_expires_in: config[:refresh_token_expires_in],
|
|
@@ -46,7 +54,8 @@ module BetterAuth
|
|
|
46
54
|
nonce: code[:nonce],
|
|
47
55
|
auth_time: code[:auth_time],
|
|
48
56
|
reference_id: code[:reference_id],
|
|
49
|
-
filter_id_token_claims_by_scope: true
|
|
57
|
+
filter_id_token_claims_by_scope: true,
|
|
58
|
+
store_tokens: config[:store_tokens]
|
|
50
59
|
)
|
|
51
60
|
when OAuthProtocol::CLIENT_CREDENTIALS_GRANT
|
|
52
61
|
requested = OAuthProtocol.parse_scopes(body["scope"])
|
|
@@ -66,19 +75,38 @@ module BetterAuth
|
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
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)
|
|
78
|
+
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, store_tokens: config[:store_tokens])
|
|
70
79
|
when OAuthProtocol::REFRESH_GRANT
|
|
71
80
|
refresh_record = OAuthProtocol.find_token_by_hint(config[:store], body["refresh_token"].to_s, "refresh_token", prefix: config[:prefix])
|
|
72
81
|
refresh_scopes = OAuthProtocol.parse_scopes(body["scope"] || refresh_record&.fetch("scopes", nil))
|
|
73
82
|
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)
|
|
83
|
+
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, store_tokens: config[:store_tokens])
|
|
75
84
|
else
|
|
76
85
|
raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type")
|
|
77
86
|
end
|
|
78
|
-
ctx.json(response)
|
|
87
|
+
ctx.json(response, headers: oauth_no_store_headers)
|
|
79
88
|
end
|
|
80
89
|
end
|
|
81
90
|
|
|
91
|
+
def oauth_no_store_headers
|
|
92
|
+
{"Cache-Control" => "no-store", "Pragma" => "no-cache"}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def oauth_active_authorization_session!(ctx, stored_session)
|
|
96
|
+
data = OAuthProtocol.stringify_keys(stored_session || {})
|
|
97
|
+
session_snapshot = OAuthProtocol.stringify_keys(data["session"] || data[:session] || {})
|
|
98
|
+
user_snapshot = OAuthProtocol.stringify_keys(data["user"] || data[:user] || {})
|
|
99
|
+
session_id = session_snapshot["id"]
|
|
100
|
+
stored = session_id && ctx.context.adapter.find_one(model: "session", where: [{field: "id", value: session_id}])
|
|
101
|
+
raise APIError.new("BAD_REQUEST", message: "session no longer exists") unless stored
|
|
102
|
+
raise APIError.new("BAD_REQUEST", message: "session no longer exists") if stored["expiresAt"] && stored["expiresAt"] <= Time.now
|
|
103
|
+
|
|
104
|
+
user = ctx.context.internal_adapter.find_user_by_id(stored["userId"] || user_snapshot["id"])
|
|
105
|
+
raise APIError.new("BAD_REQUEST", message: "missing user, user may have been deleted") unless user
|
|
106
|
+
|
|
107
|
+
{"user" => user, "session" => stored}
|
|
108
|
+
end
|
|
109
|
+
|
|
82
110
|
def oauth_validate_resource!(ctx, config, body, scopes)
|
|
83
111
|
resources = Array(body["resource"]).compact.map(&:to_s)
|
|
84
112
|
return nil if resources.empty?
|
|
@@ -36,6 +36,7 @@ module BetterAuth
|
|
|
36
36
|
singleton_class.remove_method(:oauth_provider) if singleton_class.method_defined?(:oauth_provider) || singleton_class.private_method_defined?(:oauth_provider)
|
|
37
37
|
|
|
38
38
|
def oauth_provider(options = {})
|
|
39
|
+
raw_options = normalize_hash(options)
|
|
39
40
|
config = {
|
|
40
41
|
login_page: "/login",
|
|
41
42
|
consent_page: "/oauth2/consent",
|
|
@@ -48,7 +49,8 @@ module BetterAuth
|
|
|
48
49
|
signup: {},
|
|
49
50
|
select_account: {},
|
|
50
51
|
post_login: {},
|
|
51
|
-
store_client_secret: "
|
|
52
|
+
store_client_secret: "hashed",
|
|
53
|
+
store_tokens: "hashed",
|
|
52
54
|
prefix: {},
|
|
53
55
|
code_expires_in: 600,
|
|
54
56
|
id_token_expires_in: 36_000,
|
|
@@ -58,12 +60,15 @@ module BetterAuth
|
|
|
58
60
|
client_credential_grant_default_scopes: nil,
|
|
59
61
|
scope_expirations: {},
|
|
60
62
|
store: OAuthProtocol.stores
|
|
61
|
-
}.merge(
|
|
63
|
+
}.merge(raw_options)
|
|
64
|
+
|
|
65
|
+
oauth_provider_validate_config!(config, raw_options)
|
|
62
66
|
|
|
63
67
|
Plugin.new(
|
|
64
68
|
id: "oauth-provider",
|
|
65
69
|
version: BetterAuth::OAuthProvider::VERSION,
|
|
66
70
|
init: oauth_provider_init(config),
|
|
71
|
+
hooks: oauth_provider_hooks(config),
|
|
67
72
|
endpoints: oauth_provider_endpoints(config),
|
|
68
73
|
schema: oauth_provider_schema,
|
|
69
74
|
rate_limit: oauth_provider_rate_limits(config),
|
|
@@ -71,6 +76,101 @@ module BetterAuth
|
|
|
71
76
|
)
|
|
72
77
|
end
|
|
73
78
|
|
|
79
|
+
def oauth_provider_validate_config!(config, raw_options = {})
|
|
80
|
+
provider_scopes = OAuthProtocol.parse_scopes(config[:scopes])
|
|
81
|
+
[
|
|
82
|
+
[:client_registration_allowed_scopes, config[:client_registration_allowed_scopes]],
|
|
83
|
+
[:client_registration_default_scopes, config[:client_registration_default_scopes]]
|
|
84
|
+
].each do |key, value|
|
|
85
|
+
next if value.nil?
|
|
86
|
+
|
|
87
|
+
missing = OAuthProtocol.parse_scopes(value) - provider_scopes
|
|
88
|
+
unless missing.empty?
|
|
89
|
+
raise APIError.new("BAD_REQUEST", message: "#{key} #{missing.first} not found in scopes")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
grant_types = Array(config[:grant_types]).map(&:to_s)
|
|
94
|
+
if grant_types.include?(OAuthProtocol::REFRESH_GRANT) && !grant_types.include?(OAuthProtocol::AUTH_CODE_GRANT)
|
|
95
|
+
raise APIError.new("BAD_REQUEST", message: "refresh_token grant requires authorization_code grant")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
store_client_secret = config[:store_client_secret]
|
|
99
|
+
if config[:disable_jwt_plugin] && raw_options.key?(:store_client_secret) && oauth_hashed_secret_storage?(store_client_secret)
|
|
100
|
+
raise APIError.new("BAD_REQUEST", message: "unable to store hashed secrets because id tokens will be signed with client secret")
|
|
101
|
+
end
|
|
102
|
+
if !config[:disable_jwt_plugin] && oauth_encrypted_secret_storage?(store_client_secret)
|
|
103
|
+
raise APIError.new("BAD_REQUEST", message: "encrypted secret storage is not recommended, please use hashed secret storage with the JWT plugin")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def oauth_hashed_secret_storage?(value)
|
|
108
|
+
mode = value.is_a?(Hash) ? normalize_hash(value) : value.to_s
|
|
109
|
+
mode == "hashed" || (mode.is_a?(Hash) && mode[:hash].respond_to?(:call))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def oauth_encrypted_secret_storage?(value)
|
|
113
|
+
mode = value.is_a?(Hash) ? normalize_hash(value) : value.to_s
|
|
114
|
+
mode == "encrypted" || (mode.is_a?(Hash) && (mode[:encrypt].respond_to?(:call) || mode[:decrypt].respond_to?(:call)))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def oauth_provider_hooks(config)
|
|
118
|
+
{
|
|
119
|
+
before: [
|
|
120
|
+
{
|
|
121
|
+
matcher: ->(ctx) { ctx.path.start_with?("/sign-in/", "/sign-up/") && !!oauth_query_from_body(ctx.body) },
|
|
122
|
+
handler: ->(ctx) { oauth_validate_query_hook!(ctx) }
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
after: [
|
|
126
|
+
{
|
|
127
|
+
matcher: ->(ctx) { oauth_resume_after_session_cookie?(ctx) },
|
|
128
|
+
handler: ->(ctx) { oauth_resume_after_session_cookie(ctx, config) }
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def oauth_validate_query_hook!(ctx)
|
|
135
|
+
oauth_query = oauth_query_from_body(ctx.body)
|
|
136
|
+
return unless oauth_query
|
|
137
|
+
|
|
138
|
+
unless OAuthProvider::Utils.verify_oauth_query_params(oauth_query, ctx.context.secret)
|
|
139
|
+
raise APIError.new("BAD_REQUEST", message: "invalid_signature", body: {error: "invalid_signature"})
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def oauth_resume_after_session_cookie?(ctx)
|
|
146
|
+
return false unless oauth_query_from_body(ctx.body)
|
|
147
|
+
return false unless ctx.path.start_with?("/sign-in/", "/sign-up/")
|
|
148
|
+
|
|
149
|
+
ctx.response_headers["set-cookie"].to_s.include?(ctx.context.auth_cookies[:session_token].name)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def oauth_resume_after_session_cookie(ctx, config)
|
|
153
|
+
query = oauth_verified_query!(ctx, oauth_query_from_body(ctx.body))
|
|
154
|
+
ctx.context.set_current_session(ctx.context.new_session) if ctx.context.respond_to?(:set_current_session) && ctx.context.new_session
|
|
155
|
+
location = oauth_redirect_location { oauth_authorize_flow(ctx, config, query, continue_post_login: true) }
|
|
156
|
+
[302, Endpoint::Result.merge_headers(ctx.response_headers, {"location" => location}), [""]]
|
|
157
|
+
rescue APIError => error
|
|
158
|
+
raise APIError.new(
|
|
159
|
+
error.status,
|
|
160
|
+
message: error.message,
|
|
161
|
+
headers: Endpoint::Result.merge_headers(ctx.response_headers, error.headers),
|
|
162
|
+
code: error.code,
|
|
163
|
+
body: error.body
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def oauth_query_from_body(body)
|
|
168
|
+
return nil unless body.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
data = OAuthProtocol.stringify_keys(body || {})
|
|
171
|
+
data["oauth_query"] || data["oauthQuery"]
|
|
172
|
+
end
|
|
173
|
+
|
|
74
174
|
def oauth_provider_init(config)
|
|
75
175
|
lambda do |context|
|
|
76
176
|
advertised_scopes = Array(config.dig(:advertised_metadata, :scopes_supported)).map(&:to_s)
|
|
@@ -129,5 +229,419 @@ module BetterAuth
|
|
|
129
229
|
o_auth2_end_session: oauth_end_session_endpoint
|
|
130
230
|
}
|
|
131
231
|
end
|
|
232
|
+
|
|
233
|
+
def oauth_openapi_for(route)
|
|
234
|
+
{
|
|
235
|
+
register_client: oauth_client_registration_openapi("Register a new OAuth2 client", include_secret: true),
|
|
236
|
+
create_client: oauth_client_registration_openapi("Create a new OAuth2 client", include_secret: true),
|
|
237
|
+
public_client_prelogin: oauth_public_client_prelogin_openapi,
|
|
238
|
+
delete_client: oauth_delete_client_openapi,
|
|
239
|
+
update_client: oauth_update_client_openapi,
|
|
240
|
+
rotate_client_secret: oauth_rotate_client_secret_openapi,
|
|
241
|
+
update_consent: oauth_update_consent_openapi,
|
|
242
|
+
delete_consent: oauth_delete_consent_openapi,
|
|
243
|
+
continue: oauth_continue_openapi,
|
|
244
|
+
consent: oauth_consent_openapi,
|
|
245
|
+
token: oauth_token_openapi,
|
|
246
|
+
introspect: oauth_introspect_openapi,
|
|
247
|
+
revoke: oauth_revoke_openapi,
|
|
248
|
+
end_session: oauth_end_session_openapi
|
|
249
|
+
}.fetch(route)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def oauth_client_registration_openapi(description, include_secret:)
|
|
253
|
+
{
|
|
254
|
+
openapi: {
|
|
255
|
+
description: description,
|
|
256
|
+
requestBody: OpenAPI.json_request_body(oauth_client_registration_schema),
|
|
257
|
+
responses: {
|
|
258
|
+
"201" => OpenAPI.json_response("OAuth2 client created successfully", oauth_client_response_schema(include_secret: include_secret))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def oauth_public_client_prelogin_openapi
|
|
265
|
+
{
|
|
266
|
+
openapi: {
|
|
267
|
+
description: "Get public OAuth2 client metadata before login",
|
|
268
|
+
requestBody: OpenAPI.json_request_body(
|
|
269
|
+
OpenAPI.object_schema(
|
|
270
|
+
{
|
|
271
|
+
client_id: {type: "string", description: "OAuth2 client ID"},
|
|
272
|
+
oauth_query: {type: "string", description: "Signed OAuth query string"}
|
|
273
|
+
},
|
|
274
|
+
required: ["client_id", "oauth_query"]
|
|
275
|
+
)
|
|
276
|
+
),
|
|
277
|
+
responses: {
|
|
278
|
+
"200" => OpenAPI.json_response("Public OAuth2 client metadata", oauth_public_client_schema)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def oauth_delete_client_openapi
|
|
285
|
+
{
|
|
286
|
+
openapi: {
|
|
287
|
+
description: "Delete an OAuth2 client",
|
|
288
|
+
requestBody: OpenAPI.json_request_body(oauth_client_id_body_schema),
|
|
289
|
+
responses: {
|
|
290
|
+
"200" => OpenAPI.json_response("OAuth2 client deleted", OpenAPI.object_schema({deleted: {type: "boolean"}}, required: ["deleted"]))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def oauth_update_client_openapi
|
|
297
|
+
{
|
|
298
|
+
openapi: {
|
|
299
|
+
description: "Update an OAuth2 client",
|
|
300
|
+
requestBody: OpenAPI.json_request_body(
|
|
301
|
+
OpenAPI.object_schema(
|
|
302
|
+
{
|
|
303
|
+
client_id: {type: "string", description: "OAuth2 client ID"},
|
|
304
|
+
update: oauth_client_registration_schema.merge(description: "Client metadata to update")
|
|
305
|
+
},
|
|
306
|
+
required: ["client_id", "update"]
|
|
307
|
+
)
|
|
308
|
+
),
|
|
309
|
+
responses: {
|
|
310
|
+
"200" => OpenAPI.json_response("OAuth2 client updated", oauth_client_response_schema(include_secret: false))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def oauth_rotate_client_secret_openapi
|
|
317
|
+
{
|
|
318
|
+
openapi: {
|
|
319
|
+
description: "Rotate an OAuth2 client secret",
|
|
320
|
+
requestBody: OpenAPI.json_request_body(oauth_client_id_body_schema),
|
|
321
|
+
responses: {
|
|
322
|
+
"200" => OpenAPI.json_response("OAuth2 client secret rotated", oauth_client_response_schema(include_secret: true))
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def oauth_update_consent_openapi
|
|
329
|
+
{
|
|
330
|
+
openapi: {
|
|
331
|
+
description: "Update OAuth2 consent scopes",
|
|
332
|
+
requestBody: OpenAPI.json_request_body(oauth_consent_mutation_schema(required_update: false)),
|
|
333
|
+
responses: {
|
|
334
|
+
"200" => OpenAPI.json_response("OAuth2 consent updated", oauth_consent_response_schema)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def oauth_delete_consent_openapi
|
|
341
|
+
{
|
|
342
|
+
openapi: {
|
|
343
|
+
description: "Delete OAuth2 consent",
|
|
344
|
+
requestBody: OpenAPI.json_request_body(oauth_consent_identifier_schema),
|
|
345
|
+
responses: {
|
|
346
|
+
"200" => OpenAPI.json_response("OAuth2 consent deleted", OpenAPI.object_schema({deleted: {type: "boolean"}}, required: ["deleted"]))
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def oauth_continue_openapi
|
|
353
|
+
{
|
|
354
|
+
openapi: {
|
|
355
|
+
description: "Continue an OAuth2 authorization interaction",
|
|
356
|
+
requestBody: OpenAPI.json_request_body(
|
|
357
|
+
OpenAPI.object_schema(
|
|
358
|
+
{
|
|
359
|
+
oauth_query: {type: "string", description: "Signed OAuth query string"},
|
|
360
|
+
selected: {type: "boolean", description: "Continue after account selection"},
|
|
361
|
+
created: {type: "boolean", description: "Continue after account creation"},
|
|
362
|
+
postLogin: {type: "boolean", description: "Continue after post-login flow"},
|
|
363
|
+
post_login: {type: "boolean", description: "Continue after post-login flow"}
|
|
364
|
+
},
|
|
365
|
+
required: ["oauth_query"]
|
|
366
|
+
)
|
|
367
|
+
),
|
|
368
|
+
responses: {
|
|
369
|
+
"200" => OpenAPI.json_response("OAuth2 authorization redirect", oauth_redirect_response_schema)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def oauth_consent_openapi
|
|
376
|
+
{
|
|
377
|
+
openapi: {
|
|
378
|
+
description: "Submit an OAuth2 consent decision",
|
|
379
|
+
requestBody: OpenAPI.json_request_body(
|
|
380
|
+
OpenAPI.object_schema(
|
|
381
|
+
{
|
|
382
|
+
consent_code: {type: "string", description: "Consent code issued by the authorization flow"},
|
|
383
|
+
accept: {type: "boolean", description: "Whether the user accepted the consent request"},
|
|
384
|
+
scope: {type: "string", description: "Granted scopes as a space-delimited string"},
|
|
385
|
+
scopes: {type: "array", items: {type: "string"}, description: "Granted scopes"}
|
|
386
|
+
},
|
|
387
|
+
required: ["consent_code"]
|
|
388
|
+
)
|
|
389
|
+
),
|
|
390
|
+
responses: {
|
|
391
|
+
"200" => OpenAPI.json_response("OAuth2 consent redirect", OpenAPI.object_schema({redirectURI: {type: "string"}}, required: ["redirectURI"]))
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def oauth_token_openapi
|
|
398
|
+
{
|
|
399
|
+
openapi: {
|
|
400
|
+
description: "Exchange an OAuth2 grant for tokens",
|
|
401
|
+
requestBody: OpenAPI.json_request_body(
|
|
402
|
+
OpenAPI.object_schema(
|
|
403
|
+
{
|
|
404
|
+
grant_type: {type: "string", enum: [OAuthProtocol::AUTH_CODE_GRANT, OAuthProtocol::CLIENT_CREDENTIALS_GRANT, OAuthProtocol::REFRESH_GRANT]},
|
|
405
|
+
code: {type: "string", description: "Authorization code"},
|
|
406
|
+
redirect_uri: {type: "string", format: "uri"},
|
|
407
|
+
code_verifier: {type: "string"},
|
|
408
|
+
client_id: {type: "string"},
|
|
409
|
+
client_secret: {type: "string"},
|
|
410
|
+
refresh_token: {type: "string"},
|
|
411
|
+
scope: {type: "string"},
|
|
412
|
+
resource: {oneOf: [{type: "string"}, {type: "array", items: {type: "string"}}]}
|
|
413
|
+
},
|
|
414
|
+
required: ["grant_type"]
|
|
415
|
+
)
|
|
416
|
+
),
|
|
417
|
+
responses: {
|
|
418
|
+
"200" => OpenAPI.json_response("OAuth2 tokens issued", oauth_token_response_schema)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def oauth_introspect_openapi
|
|
425
|
+
{
|
|
426
|
+
openapi: {
|
|
427
|
+
description: "Introspect an OAuth2 token",
|
|
428
|
+
requestBody: OpenAPI.json_request_body(
|
|
429
|
+
OpenAPI.object_schema(
|
|
430
|
+
{
|
|
431
|
+
token: {type: "string", description: "Token to introspect"},
|
|
432
|
+
token_type_hint: {type: "string", enum: ["access_token", "refresh_token"]}
|
|
433
|
+
},
|
|
434
|
+
required: ["token"]
|
|
435
|
+
)
|
|
436
|
+
),
|
|
437
|
+
responses: {
|
|
438
|
+
"200" => OpenAPI.json_response("OAuth2 token introspection result", oauth_introspection_response_schema)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def oauth_revoke_openapi
|
|
445
|
+
{
|
|
446
|
+
openapi: {
|
|
447
|
+
description: "Revoke an OAuth2 token",
|
|
448
|
+
requestBody: OpenAPI.json_request_body(
|
|
449
|
+
OpenAPI.object_schema(
|
|
450
|
+
{
|
|
451
|
+
token: {type: "string", description: "Token to revoke"},
|
|
452
|
+
token_type_hint: {type: "string", enum: ["access_token", "refresh_token"]}
|
|
453
|
+
},
|
|
454
|
+
required: ["token"]
|
|
455
|
+
)
|
|
456
|
+
),
|
|
457
|
+
responses: {
|
|
458
|
+
"200" => OpenAPI.json_response("OAuth2 token revoked", OpenAPI.object_schema({revoked: {type: "boolean"}}, required: ["revoked"]))
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def oauth_end_session_openapi
|
|
465
|
+
{
|
|
466
|
+
openapi: {
|
|
467
|
+
description: "End an OpenID Connect session",
|
|
468
|
+
parameters: oauth_end_session_parameters,
|
|
469
|
+
requestBody: OpenAPI.json_request_body(oauth_end_session_body_schema, required: false),
|
|
470
|
+
responses: {
|
|
471
|
+
"200" => OpenAPI.json_response("OpenID Connect session ended", OpenAPI.status_response_schema)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def oauth_client_registration_schema
|
|
478
|
+
OpenAPI.object_schema(
|
|
479
|
+
{
|
|
480
|
+
redirect_uris: {type: "array", items: {type: "string", format: "uri"}, description: "Allowed redirect URIs"},
|
|
481
|
+
post_logout_redirect_uris: {type: "array", items: {type: "string", format: "uri"}, description: "Allowed post logout redirect URIs"},
|
|
482
|
+
client_name: {type: "string", description: "OAuth2 client name"},
|
|
483
|
+
client_uri: {type: "string", format: "uri"},
|
|
484
|
+
logo_uri: {type: "string", format: "uri"},
|
|
485
|
+
contacts: {type: "array", items: {type: "string"}},
|
|
486
|
+
tos_uri: {type: "string", format: "uri"},
|
|
487
|
+
policy_uri: {type: "string", format: "uri"},
|
|
488
|
+
software_id: {type: "string"},
|
|
489
|
+
software_version: {type: "string"},
|
|
490
|
+
software_statement: {type: "string"},
|
|
491
|
+
token_endpoint_auth_method: {type: "string", enum: ["client_secret_basic", "client_secret_post", "none"]},
|
|
492
|
+
grant_types: {type: "array", items: {type: "string", enum: [OAuthProtocol::AUTH_CODE_GRANT, OAuthProtocol::CLIENT_CREDENTIALS_GRANT, OAuthProtocol::REFRESH_GRANT]}},
|
|
493
|
+
response_types: {type: "array", items: {type: "string", enum: ["code"]}},
|
|
494
|
+
scope: {type: "string"},
|
|
495
|
+
scopes: {type: "array", items: {type: "string"}},
|
|
496
|
+
type: {type: "string", enum: ["web", "native", "user-agent-based"]},
|
|
497
|
+
require_pkce: {type: "boolean"},
|
|
498
|
+
requirePKCE: {type: "boolean"},
|
|
499
|
+
subject_type: {type: "string", enum: ["public", "pairwise"]},
|
|
500
|
+
subjectType: {type: "string", enum: ["public", "pairwise"]},
|
|
501
|
+
enable_end_session: {type: "boolean"},
|
|
502
|
+
enableEndSession: {type: "boolean"},
|
|
503
|
+
skip_consent: {type: "boolean"},
|
|
504
|
+
skipConsent: {type: "boolean"},
|
|
505
|
+
metadata: {type: "object", additionalProperties: true}
|
|
506
|
+
},
|
|
507
|
+
required: ["redirect_uris"]
|
|
508
|
+
)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def oauth_client_id_body_schema
|
|
512
|
+
OpenAPI.object_schema(
|
|
513
|
+
{
|
|
514
|
+
client_id: {type: "string", description: "OAuth2 client ID"}
|
|
515
|
+
},
|
|
516
|
+
required: ["client_id"]
|
|
517
|
+
)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def oauth_client_response_schema(include_secret:)
|
|
521
|
+
properties = oauth_public_client_schema[:properties].merge(
|
|
522
|
+
redirect_uris: {type: "array", items: {type: "string", format: "uri"}},
|
|
523
|
+
post_logout_redirect_uris: {type: "array", items: {type: "string", format: "uri"}},
|
|
524
|
+
token_endpoint_auth_method: {type: "string"},
|
|
525
|
+
grant_types: {type: "array", items: {type: "string"}},
|
|
526
|
+
response_types: {type: "array", items: {type: "string"}},
|
|
527
|
+
scope: {type: "string"},
|
|
528
|
+
public: {type: "boolean"},
|
|
529
|
+
type: {type: ["string", "null"]},
|
|
530
|
+
user_id: {type: ["string", "null"]},
|
|
531
|
+
reference_id: {type: ["string", "null"]},
|
|
532
|
+
require_pkce: {type: ["boolean", "null"]},
|
|
533
|
+
subject_type: {type: ["string", "null"]},
|
|
534
|
+
metadata: {type: "object", additionalProperties: true},
|
|
535
|
+
client_id_issued_at: {type: "number"},
|
|
536
|
+
client_secret_expires_at: {type: "number"}
|
|
537
|
+
)
|
|
538
|
+
properties[:client_secret] = {type: "string", description: "OAuth2 client secret"} if include_secret
|
|
539
|
+
OpenAPI.object_schema(properties)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def oauth_public_client_schema
|
|
543
|
+
OpenAPI.object_schema(
|
|
544
|
+
{
|
|
545
|
+
client_id: {type: "string"},
|
|
546
|
+
client_name: {type: "string"},
|
|
547
|
+
client_uri: {type: ["string", "null"], format: "uri"},
|
|
548
|
+
logo_uri: {type: ["string", "null"], format: "uri"},
|
|
549
|
+
contacts: {type: "array", items: {type: "string"}},
|
|
550
|
+
tos_uri: {type: ["string", "null"], format: "uri"},
|
|
551
|
+
policy_uri: {type: ["string", "null"], format: "uri"}
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def oauth_consent_identifier_schema
|
|
557
|
+
OpenAPI.object_schema(
|
|
558
|
+
{
|
|
559
|
+
id: {type: "string", description: "OAuth2 consent ID"},
|
|
560
|
+
client_id: {type: "string", description: "OAuth2 client ID"}
|
|
561
|
+
}
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def oauth_consent_mutation_schema(required_update:)
|
|
566
|
+
OpenAPI.object_schema(
|
|
567
|
+
oauth_consent_identifier_schema[:properties].merge(
|
|
568
|
+
update: OpenAPI.object_schema({scopes: {type: "array", items: {type: "string"}}}),
|
|
569
|
+
scope: {type: "string"},
|
|
570
|
+
scopes: {type: "array", items: {type: "string"}}
|
|
571
|
+
),
|
|
572
|
+
required: required_update ? ["update"] : []
|
|
573
|
+
)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def oauth_consent_response_schema
|
|
577
|
+
OpenAPI.object_schema(
|
|
578
|
+
{
|
|
579
|
+
id: {type: "string"},
|
|
580
|
+
clientId: {type: "string"},
|
|
581
|
+
userId: {type: "string"},
|
|
582
|
+
scopes: {type: "array", items: {type: "string"}},
|
|
583
|
+
createdAt: {type: "string", format: "date-time"},
|
|
584
|
+
updatedAt: {type: "string", format: "date-time"}
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def oauth_redirect_response_schema
|
|
590
|
+
OpenAPI.object_schema(
|
|
591
|
+
{
|
|
592
|
+
redirect: {type: "boolean", enum: [true]},
|
|
593
|
+
url: {type: "string", format: "uri"}
|
|
594
|
+
},
|
|
595
|
+
required: ["redirect", "url"]
|
|
596
|
+
)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def oauth_token_response_schema
|
|
600
|
+
OpenAPI.object_schema(
|
|
601
|
+
{
|
|
602
|
+
access_token: {type: "string"},
|
|
603
|
+
token_type: {type: "string"},
|
|
604
|
+
expires_in: {type: "number"},
|
|
605
|
+
refresh_token: {type: "string"},
|
|
606
|
+
scope: {type: "string"},
|
|
607
|
+
id_token: {type: "string"}
|
|
608
|
+
},
|
|
609
|
+
required: ["access_token", "token_type"]
|
|
610
|
+
)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def oauth_introspection_response_schema
|
|
614
|
+
OpenAPI.object_schema(
|
|
615
|
+
{
|
|
616
|
+
active: {type: "boolean"},
|
|
617
|
+
client_id: {type: "string"},
|
|
618
|
+
scope: {type: "string"},
|
|
619
|
+
sub: {type: "string"},
|
|
620
|
+
iss: {type: "string"},
|
|
621
|
+
iat: {type: "number"},
|
|
622
|
+
exp: {type: "number"},
|
|
623
|
+
sid: {type: "string"},
|
|
624
|
+
aud: {oneOf: [{type: "string"}, {type: "array", items: {type: "string"}}]}
|
|
625
|
+
},
|
|
626
|
+
required: ["active"]
|
|
627
|
+
)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def oauth_end_session_parameters
|
|
631
|
+
oauth_end_session_body_schema[:properties].keys.map do |name|
|
|
632
|
+
OpenAPI.query_parameter(name.to_s, required: false, schema: oauth_end_session_body_schema[:properties][name])
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def oauth_end_session_body_schema
|
|
637
|
+
OpenAPI.object_schema(
|
|
638
|
+
{
|
|
639
|
+
id_token_hint: {type: "string"},
|
|
640
|
+
client_id: {type: "string"},
|
|
641
|
+
post_logout_redirect_uri: {type: "string", format: "uri"},
|
|
642
|
+
state: {type: "string"}
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
end
|
|
132
646
|
end
|
|
133
647
|
end
|
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.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -119,14 +119,14 @@ files:
|
|
|
119
119
|
- lib/better_auth/plugins/oauth_provider/types/zod.rb
|
|
120
120
|
- lib/better_auth/plugins/oauth_provider/userinfo.rb
|
|
121
121
|
- lib/better_auth/plugins/oauth_provider/utils/index.rb
|
|
122
|
-
homepage: https://github.com/sebasxsala/better-auth
|
|
122
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
123
123
|
licenses:
|
|
124
124
|
- MIT
|
|
125
125
|
metadata:
|
|
126
|
-
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
127
|
-
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
128
|
-
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-oauth-provider/CHANGELOG.md
|
|
129
|
-
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
126
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
127
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
128
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-oauth-provider/CHANGELOG.md
|
|
129
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
130
130
|
rdoc_options: []
|
|
131
131
|
require_paths:
|
|
132
132
|
- lib
|