rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  63. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  64. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  65. data/lib/rodauth/oauth/refinements.rb +0 -48
  66. data/templates/jwt_public_key_field.str +0 -4
  67. data/templates/oauth_application_oauth_tokens.str +0 -52
  68. data/templates/oauth_tokens.str +0 -50
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
7
+ ALLOWED_REQUEST_URI_CONTENT_TYPES = %w[application/jose application/oauth-authz-req+jwt].freeze
8
+
9
+ depends :oauth_authorize_base, :oauth_jwt_base
10
+
11
+ auth_value_method :oauth_require_request_uri_registration, false
12
+ auth_value_method :oauth_request_object_signing_alg_allow_none, false
13
+
14
+ auth_value_method :oauth_applications_request_uris_column, :request_uris
15
+
16
+ auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
17
+ auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
18
+ auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
19
+
20
+ translatable_method :oauth_invalid_request_object_message, "request object is invalid"
21
+
22
+ auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
23
+
24
+ private
25
+
26
+ # /authorize
27
+
28
+ def validate_authorize_params
29
+ request_object = param_or_nil("request")
30
+
31
+ request_uri = param_or_nil("request_uri")
32
+
33
+ return super unless (request_object || request_uri) && oauth_application
34
+
35
+ if request_uri
36
+ request_uri = CGI.unescape(request_uri)
37
+
38
+ redirect_response_error("invalid_request_uri") unless supported_request_uri?(request_uri, oauth_application)
39
+
40
+ response = http_request(request_uri)
41
+
42
+ unless response.code.to_i == 200 && ALLOWED_REQUEST_URI_CONTENT_TYPES.include?(response["content-type"])
43
+ redirect_response_error("invalid_request_uri")
44
+ end
45
+
46
+ request_object = response.body
47
+ end
48
+
49
+ request_sig_enc_opts = {
50
+ jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
51
+ jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
52
+ jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
53
+ }.compact
54
+
55
+ request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
56
+
57
+ if request_sig_enc_opts[:jws_algorithm] == "none"
58
+ jwks = nil
59
+ elsif (jwks = oauth_application_jwks(oauth_application))
60
+ jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
61
+ else
62
+ redirect_response_error("invalid_request_object")
63
+ end
64
+
65
+ claims = jwt_decode(request_object,
66
+ jwks: jwks,
67
+ verify_jti: false,
68
+ verify_iss: false,
69
+ verify_aud: false,
70
+ **request_sig_enc_opts)
71
+
72
+ redirect_response_error("invalid_request_object") unless claims
73
+
74
+ if (iss = claims["iss"]) && (iss != oauth_application[oauth_applications_client_id_column])
75
+ redirect_response_error("invalid_request_object")
76
+ end
77
+
78
+ if (aud = claims["aud"]) && !verify_aud(aud, oauth_jwt_issuer)
79
+ redirect_response_error("invalid_request_object")
80
+ end
81
+
82
+ # If signed, the Authorization Request
83
+ # Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
84
+ # as members, with their semantics being the same as defined in the JWT
85
+ # [RFC7519] specification. The value of "aud" should be the value of
86
+ # the Authorization Server (AS) "issuer" as defined in RFC8414
87
+ # [RFC8414].
88
+ claims.delete("iss")
89
+ audience = claims.delete("aud")
90
+
91
+ redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
92
+
93
+ claims.each do |k, v|
94
+ request.params[k.to_s] = v
95
+ end
96
+
97
+ super
98
+ end
99
+
100
+ def supported_request_uri?(request_uri, oauth_application)
101
+ return false unless check_valid_uri?(request_uri)
102
+
103
+ request_uris = oauth_application[oauth_applications_request_uris_column]
104
+
105
+ request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
106
+ end
107
+
108
+ def oauth_server_metadata_body(*)
109
+ super.tap do |data|
110
+ data[:request_parameter_supported] = true
111
+ data[:request_uri_parameter_supported] = true
112
+ data[:require_request_uri_registration] = oauth_require_request_uri_registration
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+
3
5
  module Rodauth
4
6
  Feature.define(:oauth_management_base, :OauthManagementBase) do
5
7
  depends :oauth_authorize_base
@@ -1,44 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rodauth/oauth/refinements"
3
+ require "rodauth/oauth"
4
4
 
5
5
  module Rodauth
6
6
  Feature.define(:oauth_pkce, :OauthPkce) do
7
- using PrefixExtensions
8
-
9
7
  depends :oauth_authorization_code_grant
10
8
 
11
- auth_value_method :use_oauth_pkce?, true
12
-
13
- auth_value_method :oauth_require_pkce, false
9
+ auth_value_method :oauth_require_pkce, true
14
10
  auth_value_method :oauth_pkce_challenge_method, "S256"
15
11
 
16
12
  auth_value_method :oauth_grants_code_challenge_column, :code_challenge
17
13
  auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
18
14
 
19
- auth_value_method :code_challenge_required_error_code, "invalid_request"
20
- translatable_method :code_challenge_required_message, "code challenge required"
21
- auth_value_method :unsupported_transform_algorithm_error_code, "invalid_request"
22
- translatable_method :unsupported_transform_algorithm_message, "transform algorithm not supported"
15
+ auth_value_method :oauth_code_challenge_required_error_code, "invalid_request"
16
+ translatable_method :oauth_code_challenge_required_message, "code challenge required"
17
+ auth_value_method :oauth_unsupported_transform_algorithm_error_code, "invalid_request"
18
+ translatable_method :oauth_unsupported_transform_algorithm_message, "transform algorithm not supported"
23
19
 
24
20
  private
25
21
 
26
- def authorized_oauth_application?(oauth_application, client_secret, _)
27
- return true if use_oauth_pkce? && param_or_nil("code_verifier")
22
+ def supports_auth_method?(oauth_application, auth_method)
23
+ return super unless auth_method == "none"
28
24
 
29
- super
25
+ request.params.key?("code_verifier") || super
30
26
  end
31
27
 
32
28
  def validate_authorize_params
33
- validate_pkce_challenge_params if use_oauth_pkce?
29
+ validate_pkce_challenge_params
34
30
 
35
31
  super
36
32
  end
37
33
 
38
34
  def create_oauth_grant(create_params = {})
39
35
  # PKCE flow
40
- if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
41
- code_challenge_method = param_or_nil("code_challenge_method")
36
+ if (code_challenge = param_or_nil("code_challenge"))
37
+ code_challenge_method = param_or_nil("code_challenge_method") || oauth_pkce_challenge_method
42
38
 
43
39
  create_params[oauth_grants_code_challenge_column] = code_challenge
44
40
  create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
@@ -47,18 +43,18 @@ module Rodauth
47
43
  super
48
44
  end
49
45
 
50
- def create_oauth_token_from_authorization_code(oauth_grant, create_params, *)
51
- if use_oauth_pkce?
52
- if oauth_grant[oauth_grants_code_challenge_column]
53
- code_verifier = param_or_nil("code_verifier")
46
+ def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
47
+ oauth_grant ||= valid_locked_oauth_grant(grant_params)
54
48
 
55
- redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
56
- elsif oauth_require_pkce
57
- redirect_response_error("code_challenge_required")
58
- end
49
+ if oauth_grant[oauth_grants_code_challenge_column]
50
+ code_verifier = param_or_nil("code_verifier")
51
+
52
+ redirect_response_error("invalid_request") unless code_verifier && check_valid_grant_challenge?(oauth_grant, code_verifier)
53
+ elsif oauth_require_pkce
54
+ redirect_response_error("code_challenge_required")
59
55
  end
60
56
 
61
- super
57
+ super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
62
58
  end
63
59
 
64
60
  def validate_pkce_challenge_params
@@ -91,7 +87,7 @@ module Rodauth
91
87
 
92
88
  def oauth_server_metadata_body(*)
93
89
  super.tap do |data|
94
- data[:code_challenge_methods_supported] = oauth_pkce_challenge_method if use_oauth_pkce?
90
+ data[:code_challenge_methods_supported] = oauth_pkce_challenge_method
95
91
  end
96
92
  end
97
93
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rodauth/oauth/version"
4
- require "rodauth/oauth/ttl_store"
3
+ require "rodauth/oauth"
5
4
 
6
5
  module Rodauth
7
6
  Feature.define(:oauth_resource_indicators, :OauthResourceIndicators) do
8
7
  depends :oauth_authorize_base
9
8
 
10
9
  auth_value_method :oauth_grants_resource_column, :resource
11
- auth_value_method :oauth_tokens_resource_column, :resource
12
10
 
13
11
  def resource_indicators
14
12
  return @resource_indicators if defined?(@resource_indicators)
@@ -38,9 +36,10 @@ module Rodauth
38
36
  def require_oauth_authorization(*)
39
37
  super
40
38
 
41
- return unless authorization_token[oauth_tokens_resource_column]
39
+ # done so to support token-in-grant-db, jwt, and resource-server mode
40
+ token_indicators = authorization_token[oauth_grants_resource_column] || authorization_token["resource"]
42
41
 
43
- token_indicators = authorization_token[oauth_tokens_resource_column]
42
+ return unless token_indicators
44
43
 
45
44
  token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
46
45
 
@@ -49,7 +48,7 @@ module Rodauth
49
48
 
50
49
  private
51
50
 
52
- def validate_oauth_token_params
51
+ def validate_token_params
53
52
  super
54
53
 
55
54
  return unless resource_indicators
@@ -59,27 +58,16 @@ module Rodauth
59
58
  end
60
59
  end
61
60
 
62
- def create_oauth_token_from_token(oauth_token, update_params)
61
+ def create_token_from_token(oauth_grant, update_params)
63
62
  return super unless resource_indicators
64
63
 
65
- return super unless oauth_token[oauth_tokens_oauth_grant_id_column]
66
-
67
- oauth_grant = db[oauth_grants_table].where(
68
- oauth_grants_id_column => oauth_token[oauth_tokens_oauth_grant_id_column],
69
- oauth_grants_revoked_at_column => nil
70
- ).first
71
-
72
64
  grant_indicators = oauth_grant[oauth_grants_resource_column]
73
65
 
74
66
  grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
75
67
 
76
68
  redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
77
69
 
78
- super(oauth_token, update_params.merge(oauth_tokens_resource_column => resource_indicators))
79
- end
80
-
81
- def check_valid_no_fragment_uri?(uri)
82
- check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
70
+ super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
83
71
  end
84
72
 
85
73
  module IndicatorAuthorizationCodeGrant
@@ -95,9 +83,11 @@ module Rodauth
95
83
  end
96
84
  end
97
85
 
98
- def create_oauth_token_from_authorization_code(oauth_grant, create_params, *args)
86
+ def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
99
87
  return super unless resource_indicators
100
88
 
89
+ oauth_grant ||= valid_locked_oauth_grant(grant_params)
90
+
101
91
  redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
102
92
 
103
93
  grant_indicators = oauth_grant[oauth_grants_resource_column]
@@ -106,7 +96,15 @@ module Rodauth
106
96
 
107
97
  redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
108
98
 
109
- super(oauth_grant, create_params.merge(oauth_tokens_resource_column => resource_indicators), *args)
99
+ # update ownership
100
+ if grant_indicators != resource_indicators
101
+ oauth_grant = __update_and_return__(
102
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column]),
103
+ oauth_grants_resource_column => resource_indicators
104
+ )
105
+ end
106
+
107
+ super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
110
108
  end
111
109
 
112
110
  def create_oauth_grant(create_params = {})
@@ -117,12 +115,12 @@ module Rodauth
117
115
  end
118
116
 
119
117
  module IndicatorIntrospection
120
- def json_token_introspect_payload(token)
121
- return super unless token[oauth_tokens_oauth_grant_id_column]
118
+ def json_token_introspect_payload(grant)
119
+ return super unless grant[oauth_grants_id_column]
122
120
 
123
121
  payload = super
124
122
 
125
- token_indicators = token[oauth_tokens_resource_column]
123
+ token_indicators = grant[oauth_grants_resource_column]
126
124
 
127
125
  token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
128
126
 
@@ -134,7 +132,7 @@ module Rodauth
134
132
  def introspection_request(*)
135
133
  payload = super
136
134
 
137
- payload[oauth_tokens_resource_column] = payload["aud"] if payload["aud"]
135
+ payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
138
136
 
139
137
  payload
140
138
  end
@@ -146,6 +144,16 @@ module Rodauth
146
144
 
147
145
  super.merge(aud: resource_indicators)
148
146
  end
147
+
148
+ def jwt_decode(token, verify_aud: true, **args)
149
+ claims = super(token, verify_aud: false, **args)
150
+
151
+ return claims unless verify_aud
152
+
153
+ return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
154
+
155
+ claims
156
+ end
149
157
  end
150
158
 
151
159
  def self.included(rodauth)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oauth_resource_server, :OauthResourceServer) do
7
+ depends :oauth_token_introspection
8
+
9
+ auth_value_method :is_authorization_server?, false
10
+
11
+ auth_value_methods(
12
+ :before_introspection_request
13
+ )
14
+
15
+ def authorization_token
16
+ return @authorization_token if defined?(@authorization_token)
17
+
18
+ # check if there is a token
19
+ access_token = fetch_access_token
20
+
21
+ return unless access_token
22
+
23
+ # where in resource server, NOT the authorization server.
24
+ payload = introspection_request("access_token", access_token)
25
+
26
+ return unless payload["active"]
27
+
28
+ @authorization_token = payload
29
+ end
30
+
31
+ def require_oauth_authorization(*scopes)
32
+ authorization_required unless authorization_token
33
+
34
+ aux_scopes = authorization_token["scope"]
35
+
36
+ token_scopes = if aux_scopes
37
+ aux_scopes.split(oauth_scope_separator)
38
+ else
39
+ []
40
+ end
41
+
42
+ authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
43
+ end
44
+
45
+ private
46
+
47
+ def introspection_request(token_type_hint, token)
48
+ introspect_url = URI("#{authorization_server_url}#{introspect_path}")
49
+
50
+ response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
51
+ before_introspection_request(request)
52
+ end
53
+
54
+ JSON.parse(response.body)
55
+ end
56
+
57
+ def before_introspection_request(request); end
58
+ end
59
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "onelogin/ruby-saml"
4
+ require "rodauth/oauth"
4
5
 
5
6
  module Rodauth
6
7
  Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
@@ -16,12 +17,18 @@ module Rodauth
16
17
  auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
17
18
  auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
18
19
 
20
+ auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
21
+
19
22
  auth_value_methods(
20
23
  :require_oauth_application_from_saml2_bearer_assertion_issuer,
21
24
  :require_oauth_application_from_saml2_bearer_assertion_subject,
22
25
  :account_from_saml2_bearer_assertion
23
26
  )
24
27
 
28
+ def oauth_grant_types_supported
29
+ super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
30
+ end
31
+
25
32
  private
26
33
 
27
34
  def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
@@ -94,7 +101,6 @@ module Rodauth
94
101
 
95
102
  def oauth_server_metadata_body(*)
96
103
  super.tap do |data|
97
- data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:saml2-bearer"
98
104
  data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
99
105
  end
100
106
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+ require "rodauth/oauth/http_extensions"
5
+
3
6
  module Rodauth
4
7
  Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
5
8
  depends :oauth_base
@@ -7,47 +10,44 @@ module Rodauth
7
10
  before "introspect"
8
11
 
9
12
  auth_value_methods(
10
- :before_introspection_request
13
+ :resource_owner_identifier
11
14
  )
12
15
 
13
16
  # /introspect
14
- route(:introspect) do |r|
15
- next unless is_authorization_server?
16
-
17
+ auth_server_route(:introspect) do |r|
18
+ require_oauth_application_for_introspect
17
19
  before_introspect_route
18
- require_oauth_application
19
20
 
20
21
  r.post do
21
22
  catch_error do
22
- validate_oauth_introspect_params
23
+ validate_introspect_params
24
+
25
+ token_type_hint = param_or_nil("token_type_hint")
23
26
 
24
27
  before_introspect
25
- oauth_token = case param("token_type_hint")
26
- when "access_token"
27
- oauth_token_by_token(param("token"))
28
+ oauth_grant = case token_type_hint
29
+ when "access_token", nil
30
+ if features.include?(:oauth_jwt) && oauth_jwt_access_tokens
31
+ jwt_decode(param("token"))
32
+ else
33
+ oauth_grant_by_token(param("token"))
34
+ end
28
35
  when "refresh_token"
29
- oauth_token_by_refresh_token(param("token"))
30
- else
31
- oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
36
+ oauth_grant_by_refresh_token(param("token"))
32
37
  end
33
38
 
34
- if oauth_application
35
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
36
- elsif oauth_token
37
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
38
- oauth_token[oauth_tokens_oauth_application_id_column]).first
39
- end
39
+ oauth_grant ||= oauth_grant_by_refresh_token(param("token")) if token_type_hint.nil?
40
40
 
41
- json_response_success(json_token_introspect_payload(oauth_token))
41
+ json_response_success(json_token_introspect_payload(oauth_grant))
42
42
  end
43
43
 
44
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
44
+ throw_json_response_error(oauth_invalid_response_status, "invalid_request")
45
45
  end
46
46
  end
47
47
 
48
48
  # Token introspect
49
49
 
50
- def validate_oauth_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
50
+ def validate_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
51
51
  # check if valid token hint type
52
52
  if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
53
53
  redirect_response_error("unsupported_token_type")
@@ -56,17 +56,35 @@ module Rodauth
56
56
  redirect_response_error("invalid_request") unless param_or_nil("token")
57
57
  end
58
58
 
59
- def json_token_introspect_payload(token)
60
- return { active: false } unless token
61
-
62
- {
63
- active: true,
64
- scope: token[oauth_tokens_scopes_column],
65
- client_id: oauth_application[oauth_applications_client_id_column],
66
- # username
67
- token_type: oauth_token_type,
68
- exp: token[oauth_tokens_expires_in_column].to_i
69
- }
59
+ def json_token_introspect_payload(grant_or_claims)
60
+ return { active: false } unless grant_or_claims
61
+
62
+ if grant_or_claims["sub"]
63
+ # JWT
64
+ {
65
+ active: true,
66
+ scope: grant_or_claims["scope"],
67
+ client_id: grant_or_claims["client_id"],
68
+ username: resource_owner_identifier(grant_or_claims),
69
+ token_type: "access_token",
70
+ exp: grant_or_claims["exp"],
71
+ iat: grant_or_claims["iat"],
72
+ nbf: grant_or_claims["nbf"],
73
+ sub: grant_or_claims["sub"],
74
+ aud: grant_or_claims["aud"],
75
+ iss: grant_or_claims["iss"],
76
+ jti: grant_or_claims["jti"]
77
+ }
78
+ else
79
+ {
80
+ active: true,
81
+ scope: grant_or_claims[oauth_grants_scopes_column],
82
+ client_id: oauth_application[oauth_applications_client_id_column],
83
+ username: resource_owner_identifier(grant_or_claims),
84
+ token_type: oauth_token_type,
85
+ exp: grant_or_claims[oauth_grants_expires_in_column].to_i
86
+ }
87
+ end
70
88
  end
71
89
 
72
90
  def check_csrf?
@@ -80,24 +98,17 @@ module Rodauth
80
98
 
81
99
  private
82
100
 
83
- def introspection_request(token_type_hint, token)
84
- auth_url = URI(authorization_server_url)
85
- http = Net::HTTP.new(auth_url.host, auth_url.port)
86
- http.use_ssl = auth_url.scheme == "https"
101
+ def require_oauth_application_for_introspect
102
+ (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Bearer (.*)\Z/, 1]))
87
103
 
88
- request = Net::HTTP::Post.new(auth_url.path + introspect_path)
89
- request["content-type"] = "application/x-www-form-urlencoded"
90
- request["accept"] = json_response_content_type
91
- request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
104
+ return require_oauth_application unless token
92
105
 
93
- before_introspection_request(request)
94
- response = http.request(request)
95
- authorization_required unless response.code.to_i == 200
106
+ oauth_application = current_oauth_application
96
107
 
97
- JSON.parse(response.body)
98
- end
108
+ authorization_required unless oauth_application
99
109
 
100
- def before_introspection_request(request); end
110
+ @oauth_application = oauth_application
111
+ end
101
112
 
102
113
  def oauth_server_metadata_body(*)
103
114
  super.tap do |data|
@@ -105,5 +116,24 @@ module Rodauth
105
116
  data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
106
117
  end
107
118
  end
119
+
120
+ def resource_owner_identifier(grant_or_claims)
121
+ if (account_id = grant_or_claims[oauth_grants_account_id_column])
122
+ account_ds(account_id).select(login_column).first[login_column]
123
+ elsif (app_id = grant_or_claims[oauth_grants_oauth_application_id_column])
124
+ db[oauth_applications_table].where(oauth_applications_id_column => app_id)
125
+ .select(oauth_applications_name_column)
126
+ .first[oauth_applications_name_column]
127
+ elsif (subject = grant_or_claims["sub"])
128
+ # JWT
129
+ if subject == grant_or_claims["client_id"]
130
+ db[oauth_applications_table].where(oauth_applications_client_id_column => subject)
131
+ .select(oauth_applications_name_column)
132
+ .first[oauth_applications_name_column]
133
+ else
134
+ account_ds(subject).select(login_column).first[login_column]
135
+ end
136
+ end
137
+ end
108
138
  end
109
139
  end