rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

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.
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