rodauth-oauth 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|