rodauth-oauth 0.0.5 → 0.0.6
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 +23 -0
- data/README.md +2 -1
- data/lib/rodauth/features/oauth.rb +19 -43
- data/lib/rodauth/features/oauth_jwt.rb +74 -15
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 73f73e4143c1c646b3a6f2a5e87fb925f38f18e157366d7522328b5cbcd2fbb7
|
4
|
+
data.tar.gz: bce8a4532e365328bb46197e72b05b78beca19b00587630f234cff0c8d51bc35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46053f71e35baad7b3c217bfbca0a259d07fd6909713805db23ff92c0503c0907903483e659fe988af774b26a87b274f8b72006983b1acc191bccb7d00919a86
|
7
|
+
data.tar.gz: 35305a71ea2b4035933d93e3fa5a3e9b388f32b2301ff05b87a86af45defa494cefc4d6d9adb823822c82acb995e57794fb710b1935aaab10430c1898d1a55b0
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,29 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
### 0.0.6
|
6
|
+
|
7
|
+
#### Features
|
8
|
+
|
9
|
+
The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (see https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20). This means that client applications can send the authorization parameters inside a signed JWT. The client applications keeps the private key, while the authorization server **must** store a public key for the client application. For encrypted JWTs, the client application should use one of the public encryption keys exposed in the JWKs URI, to encrypt the JWT. Remember, **tokens must be signed then encrypted** (or just signed).
|
10
|
+
|
11
|
+
###### Options:
|
12
|
+
|
13
|
+
* `:oauth_application_jws_jwk_column`: db column where the public key is stored; since it's stored in the JWS format, it can be stored either as a String (JSON-encoded), or as an hstore (if you're using postgresql);
|
14
|
+
* `:oauth_jwt_jwe_key`: key used to decrypt the request JWT;
|
15
|
+
* `:oauth_jwt_jwe_public_key`: key used to encrypt the request JWT, and which will be exposed in the JWKs URI in the JWK format;
|
16
|
+
|
17
|
+
|
18
|
+
#### Improvements
|
19
|
+
|
20
|
+
* Removing all `_param` options; these defined the URL params, however we're using protocol-defined params, so it's unlikely (and undesired) that these'll change.
|
21
|
+
* Hitting the revoke endpoint with a JWT access token returns a 400 error;
|
22
|
+
|
23
|
+
#### Chore
|
24
|
+
|
25
|
+
Removed React Javascript from example applications.
|
26
|
+
|
27
|
+
|
5
28
|
### 0.0.5 (26/6/2020)
|
6
29
|
|
7
30
|
#### Features
|
data/README.md
CHANGED
@@ -21,6 +21,7 @@ This gem implements:
|
|
21
21
|
* Access Type (Token refresh online and offline);
|
22
22
|
* [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
|
23
23
|
* [JWT Acess Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
|
24
|
+
* [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
|
24
25
|
* OAuth application and token management dashboards;
|
25
26
|
|
26
27
|
This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
|
@@ -89,7 +90,7 @@ You'll have to do a bit more boilerplate, so here's the instructions.
|
|
89
90
|
|
90
91
|
### Example (TL;DR)
|
91
92
|
|
92
|
-
If you're familiar with the technology and want to skip the next paragraphs, just [check our
|
93
|
+
If you're familiar with the technology and want to skip the next paragraphs, just [check our example applications](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/examples/).
|
93
94
|
|
94
95
|
|
95
96
|
Generating tokens happens mostly server-to-server, so here's an example using:
|
@@ -189,20 +189,6 @@ module Rodauth
|
|
189
189
|
|
190
190
|
auth_value_methods(:only_json?)
|
191
191
|
|
192
|
-
redirect(:oauth_application) do |id|
|
193
|
-
"/#{oauth_applications_path}/#{id}"
|
194
|
-
end
|
195
|
-
|
196
|
-
redirect(:require_authorization) do
|
197
|
-
if logged_in?
|
198
|
-
oauth_authorize_path
|
199
|
-
elsif respond_to?(:login_redirect)
|
200
|
-
login_redirect
|
201
|
-
else
|
202
|
-
default_redirect
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
192
|
auth_value_method :json_request_regexp, %r{\bapplication/(?:vnd\.api\+)?json\b}i
|
207
193
|
|
208
194
|
SERVER_METADATA = OAuth::TtlStore.new
|
@@ -246,18 +232,10 @@ module Rodauth
|
|
246
232
|
@scope = scope
|
247
233
|
end
|
248
234
|
|
249
|
-
def state
|
250
|
-
param_or_nil("state")
|
251
|
-
end
|
252
|
-
|
253
235
|
def scopes
|
254
236
|
(param_or_nil("scope") || oauth_application_default_scope).split(" ")
|
255
237
|
end
|
256
238
|
|
257
|
-
def client_id
|
258
|
-
param_or_nil("client_id")
|
259
|
-
end
|
260
|
-
|
261
239
|
def redirect_uri
|
262
240
|
param_or_nil("redirect_uri") || begin
|
263
241
|
return unless oauth_application
|
@@ -267,14 +245,6 @@ module Rodauth
|
|
267
245
|
end
|
268
246
|
end
|
269
247
|
|
270
|
-
def token_type_hint
|
271
|
-
param_or_nil("token_type_hint") || "access_token"
|
272
|
-
end
|
273
|
-
|
274
|
-
def token
|
275
|
-
param_or_nil("token")
|
276
|
-
end
|
277
|
-
|
278
248
|
def oauth_application
|
279
249
|
return @oauth_application if defined?(@oauth_application)
|
280
250
|
|
@@ -348,6 +318,8 @@ module Rodauth
|
|
348
318
|
|
349
319
|
request.on(oauth_applications_id_pattern) do |id|
|
350
320
|
oauth_application = db[oauth_applications_table].where(oauth_applications_id_column => id).first
|
321
|
+
next unless oauth_application
|
322
|
+
|
351
323
|
scope.instance_variable_set(:@oauth_application, oauth_application)
|
352
324
|
|
353
325
|
request.is do
|
@@ -359,7 +331,9 @@ module Rodauth
|
|
359
331
|
request.on(oauth_tokens_path) do
|
360
332
|
oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
|
361
333
|
scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
|
362
|
-
|
334
|
+
request.get do
|
335
|
+
oauth_tokens_view
|
336
|
+
end
|
363
337
|
end
|
364
338
|
end
|
365
339
|
|
@@ -377,7 +351,7 @@ module Rodauth
|
|
377
351
|
id = create_oauth_application
|
378
352
|
after_create_oauth_application
|
379
353
|
set_notice_flash create_oauth_application_notice_flash
|
380
|
-
redirect
|
354
|
+
redirect "#{request.path}/#{id}"
|
381
355
|
end
|
382
356
|
end
|
383
357
|
set_error_flash create_oauth_application_error_flash
|
@@ -877,8 +851,8 @@ module Rodauth
|
|
877
851
|
|
878
852
|
def validate_oauth_introspect_params
|
879
853
|
# check if valid token hint type
|
880
|
-
if token_type_hint
|
881
|
-
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(token_type_hint)
|
854
|
+
if param_or_nil("token_type_hint")
|
855
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
882
856
|
end
|
883
857
|
|
884
858
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
@@ -906,17 +880,20 @@ module Rodauth
|
|
906
880
|
|
907
881
|
def validate_oauth_revoke_params
|
908
882
|
# check if valid token hint type
|
909
|
-
|
883
|
+
if param_or_nil("token_type_hint")
|
884
|
+
redirect_response_error("unsupported_token_type") unless TOKEN_HINT_TYPES.include?(param("token_type_hint"))
|
885
|
+
end
|
910
886
|
|
911
887
|
redirect_response_error("invalid_request") unless param_or_nil("token")
|
912
888
|
end
|
913
889
|
|
914
890
|
def revoke_oauth_token
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
when "refresh_token"
|
891
|
+
token = param("token")
|
892
|
+
|
893
|
+
oauth_token = if param("token_type_hint") == "refresh_token"
|
919
894
|
oauth_token_by_refresh_token(token)
|
895
|
+
else
|
896
|
+
oauth_token_by_token(token)
|
920
897
|
end
|
921
898
|
|
922
899
|
redirect_response_error("invalid_request") unless oauth_token
|
@@ -1018,7 +995,7 @@ module Rodauth
|
|
1018
995
|
throw_json_response_error(authorization_required_error_status, "invalid_client")
|
1019
996
|
else
|
1020
997
|
set_redirect_error_flash(require_authorization_error_flash)
|
1021
|
-
redirect(
|
998
|
+
redirect(oauth_authorize_path)
|
1022
999
|
end
|
1023
1000
|
end
|
1024
1001
|
|
@@ -1186,7 +1163,6 @@ module Rodauth
|
|
1186
1163
|
route(:oauth_revoke) do |r|
|
1187
1164
|
before_revoke
|
1188
1165
|
|
1189
|
-
# access-token
|
1190
1166
|
r.post do
|
1191
1167
|
catch_error do
|
1192
1168
|
validate_oauth_revoke_params
|
@@ -1208,7 +1184,7 @@ module Rodauth
|
|
1208
1184
|
end
|
1209
1185
|
end
|
1210
1186
|
|
1211
|
-
|
1187
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
1212
1188
|
end
|
1213
1189
|
end
|
1214
1190
|
|
@@ -1253,7 +1229,7 @@ module Rodauth
|
|
1253
1229
|
end
|
1254
1230
|
|
1255
1231
|
redirect_url = URI.parse(redirect_uri)
|
1256
|
-
query_params << "state=#{state}" if state
|
1232
|
+
query_params << "state=#{param('state')}" if param_or_nil("state")
|
1257
1233
|
query_params << redirect_url.query if redirect_url.query
|
1258
1234
|
redirect_url.query = query_params.join("&") unless query_params.empty?
|
1259
1235
|
redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
|
@@ -8,6 +8,8 @@ module Rodauth
|
|
8
8
|
|
9
9
|
auth_value_method :oauth_jwt_token_issuer, "Example"
|
10
10
|
|
11
|
+
auth_value_method :oauth_application_jws_jwk_column, nil
|
12
|
+
|
11
13
|
auth_value_method :oauth_jwt_key, nil
|
12
14
|
auth_value_method :oauth_jwt_public_key, nil
|
13
15
|
auth_value_method :oauth_jwt_algorithm, "HS256"
|
@@ -20,6 +22,9 @@ module Rodauth
|
|
20
22
|
auth_value_method :oauth_jwt_jwe_copyright, nil
|
21
23
|
auth_value_method :oauth_jwt_audience, nil
|
22
24
|
|
25
|
+
auth_value_method :request_uri_not_supported_message, "request uri is unsupported"
|
26
|
+
auth_value_method :invalid_request_object_message, "request object is invalid"
|
27
|
+
|
23
28
|
auth_value_methods(
|
24
29
|
:jwt_encode,
|
25
30
|
:jwt_decode,
|
@@ -60,6 +65,48 @@ module Rodauth
|
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
68
|
+
# /authorize
|
69
|
+
|
70
|
+
def validate_oauth_grant_params
|
71
|
+
# TODO: add support for requst_uri
|
72
|
+
redirect_response_error("request_uri_not_supported") if param_or_nil("request_uri")
|
73
|
+
|
74
|
+
request_object = param_or_nil("request")
|
75
|
+
|
76
|
+
return super unless request_object && oauth_application
|
77
|
+
|
78
|
+
jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
|
79
|
+
jwk = oauth_application[oauth_application_jws_jwk_column]
|
80
|
+
|
81
|
+
if jwk
|
82
|
+
jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
redirect_response_error("invalid_request_object")
|
86
|
+
end
|
87
|
+
|
88
|
+
claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
|
89
|
+
|
90
|
+
redirect_response_error("invalid_request_object") unless claims
|
91
|
+
|
92
|
+
# If signed, the Authorization Request
|
93
|
+
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
|
94
|
+
# as members, with their semantics being the same as defined in the JWT
|
95
|
+
# [RFC7519] specification. The value of "aud" should be the value of
|
96
|
+
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
97
|
+
# [RFC8414].
|
98
|
+
claims.delete(:iss)
|
99
|
+
audience = claims.delete(:aud)
|
100
|
+
|
101
|
+
redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
|
102
|
+
|
103
|
+
claims.each do |k, v|
|
104
|
+
request.params[k.to_s] = v
|
105
|
+
end
|
106
|
+
|
107
|
+
super
|
108
|
+
end
|
109
|
+
|
63
110
|
# /token
|
64
111
|
|
65
112
|
def before_token
|
@@ -234,6 +281,10 @@ module Rodauth
|
|
234
281
|
if defined?(JSON::JWT)
|
235
282
|
# :nocov:
|
236
283
|
|
284
|
+
def jwk_import(data)
|
285
|
+
JSON::JWK.new(data)
|
286
|
+
end
|
287
|
+
|
237
288
|
# json-jwt
|
238
289
|
def jwt_encode(payload)
|
239
290
|
jwt = JSON::JWT.new(payload)
|
@@ -251,15 +302,11 @@ module Rodauth
|
|
251
302
|
jwt.to_s
|
252
303
|
end
|
253
304
|
|
254
|
-
def jwt_decode(token)
|
255
|
-
return @jwt_token if defined?(@jwt_token)
|
256
|
-
|
305
|
+
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
|
257
306
|
token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
|
258
307
|
|
259
|
-
|
260
|
-
|
261
|
-
@jwt_token = if jwk
|
262
|
-
JSON::JWT.decode(token, jwk)
|
308
|
+
@jwt_token = if jws_key
|
309
|
+
JSON::JWT.decode(token, jws_key)
|
263
310
|
elsif !is_authorization_server? && auth_server_jwks_set
|
264
311
|
JSON::JWT.decode(token, JSON::JWK::Set.new(auth_server_jwks_set))
|
265
312
|
end
|
@@ -279,6 +326,10 @@ module Rodauth
|
|
279
326
|
|
280
327
|
# ruby-jwt
|
281
328
|
|
329
|
+
def jwk_import(data)
|
330
|
+
JWT::JWK.import(data).keypair
|
331
|
+
end
|
332
|
+
|
282
333
|
def jwt_encode(payload)
|
283
334
|
headers = {}
|
284
335
|
|
@@ -312,17 +363,13 @@ module Rodauth
|
|
312
363
|
token
|
313
364
|
end
|
314
365
|
|
315
|
-
def jwt_decode(token)
|
316
|
-
return @jwt_token if defined?(@jwt_token)
|
317
|
-
|
366
|
+
def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
|
318
367
|
# decrypt jwe
|
319
368
|
token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
|
320
369
|
|
321
370
|
# decode jwt
|
322
|
-
|
323
|
-
|
324
|
-
@jwt_token = if key
|
325
|
-
JWT.decode(token, key, true, algorithms: [oauth_jwt_algorithm]).first
|
371
|
+
@jwt_token = if jws_key
|
372
|
+
JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
|
326
373
|
elsif !is_authorization_server? && auth_server_jwks_set
|
327
374
|
algorithms = auth_server_jwks_set[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
|
328
375
|
JWT.decode(token, nil, true, jwks: auth_server_jwks_set, algorithms: algorithms).first
|
@@ -339,11 +386,15 @@ module Rodauth
|
|
339
386
|
end
|
340
387
|
else
|
341
388
|
# :nocov:
|
389
|
+
def jwk_import(_data)
|
390
|
+
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
391
|
+
end
|
392
|
+
|
342
393
|
def jwt_encode(_token)
|
343
394
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
344
395
|
end
|
345
396
|
|
346
|
-
def jwt_decode(_token)
|
397
|
+
def jwt_decode(_token, **)
|
347
398
|
raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
|
348
399
|
end
|
349
400
|
|
@@ -353,6 +404,14 @@ module Rodauth
|
|
353
404
|
# :nocov:
|
354
405
|
end
|
355
406
|
|
407
|
+
def validate_oauth_revoke_params
|
408
|
+
token_hint = param_or_nil("token_type_hint")
|
409
|
+
|
410
|
+
throw(:rodauth_error) if !token_hint || token_hint == "access_token"
|
411
|
+
|
412
|
+
super
|
413
|
+
end
|
414
|
+
|
356
415
|
route(:oauth_jwks) do |r|
|
357
416
|
r.get do
|
358
417
|
json_response_success({ keys: jwks_set })
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rodauth-oauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06
|
11
|
+
date: 2020-07-06 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implementation of the OAuth 2.0 protocol on top of rodauth.
|
14
14
|
email:
|