rodauth-oauth 0.10.3 → 1.0.0.pre.beta1

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 +22 -30
  4. data/doc/release_notes/0_10_3.md +1 -1
  5. data/doc/release_notes/0_10_4.md +11 -0
  6. data/doc/release_notes/1_0_0_beta1.md +38 -0
  7. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  15. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  16. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
  17. data/lib/rodauth/features/oauth_application_management.rb +59 -72
  18. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  19. data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
  20. data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
  21. data/lib/rodauth/features/oauth_base.rb +365 -302
  22. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  23. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  24. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
  25. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  26. data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
  27. data/lib/rodauth/features/oauth_jwt.rb +52 -678
  28. data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
  29. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
  30. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  31. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
  32. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  33. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  34. data/lib/rodauth/features/oauth_resource_indicators.rb +39 -22
  35. data/lib/rodauth/features/oauth_resource_server.rb +38 -0
  36. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
  37. data/lib/rodauth/features/oauth_token_introspection.rb +76 -45
  38. data/lib/rodauth/features/oauth_token_revocation.rb +46 -31
  39. data/lib/rodauth/features/oidc.rb +188 -95
  40. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +61 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/version.rb +1 -1
  45. data/lib/rodauth/oauth.rb +29 -1
  46. data/locales/en.yml +32 -22
  47. data/locales/pt.yml +32 -22
  48. data/templates/authorize.str +19 -24
  49. data/templates/device_search.str +1 -1
  50. data/templates/device_verification.str +2 -2
  51. data/templates/jwks_field.str +1 -0
  52. data/templates/new_oauth_application.str +1 -2
  53. data/templates/oauth_application.str +2 -2
  54. data/templates/oauth_application_oauth_grants.str +54 -0
  55. data/templates/oauth_applications.str +2 -2
  56. data/templates/oauth_grants.str +52 -0
  57. metadata +21 -17
  58. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  59. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  61. data/lib/rodauth/features/oauth.rb +0 -9
  62. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  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
@@ -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)
@@ -20,7 +18,12 @@ module Rodauth
20
18
  if json_request? || param_or_nil("request") # signed request
21
19
  resources = Array(resources)
22
20
  else
23
- query = request.form_data? ? request.body.read : request.query_string
21
+ query = if request.form_data?
22
+ request.body.rewind
23
+ request.body.read
24
+ else
25
+ request.query_string
26
+ end
24
27
  # resource query param does not conform to rack parsing rules
25
28
  resources = URI.decode_www_form(query).each_with_object([]) do |(k, v), memo|
26
29
  memo << v if k == "resource"
@@ -33,9 +36,10 @@ module Rodauth
33
36
  def require_oauth_authorization(*)
34
37
  super
35
38
 
36
- 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"]
37
41
 
38
- token_indicators = authorization_token[oauth_tokens_resource_column]
42
+ return unless token_indicators
39
43
 
40
44
  token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
41
45
 
@@ -44,7 +48,7 @@ module Rodauth
44
48
 
45
49
  private
46
50
 
47
- def validate_oauth_token_params
51
+ def validate_token_params
48
52
  super
49
53
 
50
54
  return unless resource_indicators
@@ -54,23 +58,16 @@ module Rodauth
54
58
  end
55
59
  end
56
60
 
57
- def create_oauth_token_from_token(oauth_token, update_params)
61
+ def create_token_from_token(oauth_grant, update_params)
58
62
  return super unless resource_indicators
59
63
 
60
- return super unless oauth_token[oauth_tokens_oauth_grant_id_column]
61
-
62
- oauth_grant = db[oauth_grants_table].where(
63
- oauth_grants_id_column => oauth_token[oauth_tokens_oauth_grant_id_column],
64
- oauth_grants_revoked_at_column => nil
65
- ).first
66
-
67
64
  grant_indicators = oauth_grant[oauth_grants_resource_column]
68
65
 
69
66
  grant_indicators = grant_indicators.split(" ") if grant_indicators.is_a?(String)
70
67
 
71
68
  redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
72
69
 
73
- super(oauth_token, update_params.merge(oauth_tokens_resource_column => resource_indicators))
70
+ super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
74
71
  end
75
72
 
76
73
  def check_valid_no_fragment_uri?(uri)
@@ -90,9 +87,11 @@ module Rodauth
90
87
  end
91
88
  end
92
89
 
93
- def create_oauth_token_from_authorization_code(oauth_grant, create_params, *args)
90
+ def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
94
91
  return super unless resource_indicators
95
92
 
93
+ oauth_grant ||= valid_locked_oauth_grant(grant_params)
94
+
96
95
  redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
97
96
 
98
97
  grant_indicators = oauth_grant[oauth_grants_resource_column]
@@ -101,7 +100,15 @@ module Rodauth
101
100
 
102
101
  redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
103
102
 
104
- super(oauth_grant, create_params.merge(oauth_tokens_resource_column => resource_indicators), *args)
103
+ # update ownership
104
+ if grant_indicators != resource_indicators
105
+ oauth_grant = __update_and_return__(
106
+ db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column]),
107
+ oauth_grants_resource_column => resource_indicators
108
+ )
109
+ end
110
+
111
+ super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
105
112
  end
106
113
 
107
114
  def create_oauth_grant(create_params = {})
@@ -112,12 +119,12 @@ module Rodauth
112
119
  end
113
120
 
114
121
  module IndicatorIntrospection
115
- def json_token_introspect_payload(token)
116
- return super unless token[oauth_tokens_oauth_grant_id_column]
122
+ def json_token_introspect_payload(grant)
123
+ return super unless grant[oauth_grants_id_column]
117
124
 
118
125
  payload = super
119
126
 
120
- token_indicators = token[oauth_tokens_resource_column]
127
+ token_indicators = grant[oauth_grants_resource_column]
121
128
 
122
129
  token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
123
130
 
@@ -129,7 +136,7 @@ module Rodauth
129
136
  def introspection_request(*)
130
137
  payload = super
131
138
 
132
- payload[oauth_tokens_resource_column] = payload["aud"] if payload["aud"]
139
+ payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
133
140
 
134
141
  payload
135
142
  end
@@ -141,6 +148,16 @@ module Rodauth
141
148
 
142
149
  super.merge(aud: resource_indicators)
143
150
  end
151
+
152
+ def jwt_decode(token, verify_aud: true, **args)
153
+ claims = super(token, verify_aud: false, **args)
154
+
155
+ return claims unless verify_aud
156
+
157
+ return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
158
+
159
+ claims
160
+ end
144
161
  end
145
162
 
146
163
  def self.included(rodauth)
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rodauth/oauth"
4
+
3
5
  module Rodauth
4
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
+
5
15
  def authorization_token
6
16
  return @authorization_token if defined?(@authorization_token)
7
17
 
@@ -17,5 +27,33 @@ module Rodauth
17
27
 
18
28
  @authorization_token = payload
19
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
20
58
  end
21
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
@@ -22,6 +23,10 @@ module Rodauth
22
23
  :account_from_saml2_bearer_assertion
23
24
  )
24
25
 
26
+ def oauth_grant_types_supported
27
+ super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
28
+ end
29
+
25
30
  private
26
31
 
27
32
  def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
@@ -94,7 +99,6 @@ module Rodauth
94
99
 
95
100
  def oauth_server_metadata_body(*)
96
101
  super.tap do |data|
97
- data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:saml2-bearer"
98
102
  data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
99
103
  end
100
104
  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,46 +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
20
 
19
21
  r.post do
20
22
  catch_error do
21
- validate_oauth_introspect_params
23
+ validate_introspect_params
24
+
25
+ token_type_hint = param_or_nil("token_type_hint")
22
26
 
23
27
  before_introspect
24
- oauth_token = case param("token_type_hint")
25
- when "access_token"
26
- 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
27
35
  when "refresh_token"
28
- oauth_token_by_refresh_token(param("token"))
29
- else
30
- oauth_token_by_token(param("token")) || oauth_token_by_refresh_token(param("token"))
36
+ oauth_grant_by_refresh_token(param("token"))
31
37
  end
32
38
 
33
- if oauth_application
34
- redirect_response_error("invalid_request") if oauth_token && !token_from_application?(oauth_token, oauth_application)
35
- elsif oauth_token
36
- @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
37
- oauth_token[oauth_tokens_oauth_application_id_column]).first
38
- end
39
+ oauth_grant ||= oauth_grant_by_refresh_token(param("token")) if token_type_hint.nil?
39
40
 
40
- json_response_success(json_token_introspect_payload(oauth_token))
41
+ json_response_success(json_token_introspect_payload(oauth_grant))
41
42
  end
42
43
 
43
- throw_json_response_error(invalid_oauth_response_status, "invalid_request")
44
+ throw_json_response_error(oauth_invalid_response_status, "invalid_request")
44
45
  end
45
46
  end
46
47
 
47
48
  # Token introspect
48
49
 
49
- 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)
50
51
  # check if valid token hint type
51
52
  if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
52
53
  redirect_response_error("unsupported_token_type")
@@ -55,17 +56,35 @@ module Rodauth
55
56
  redirect_response_error("invalid_request") unless param_or_nil("token")
56
57
  end
57
58
 
58
- def json_token_introspect_payload(token)
59
- return { active: false } unless token
60
-
61
- {
62
- active: true,
63
- scope: token[oauth_tokens_scopes_column],
64
- client_id: oauth_application[oauth_applications_client_id_column],
65
- # username
66
- token_type: oauth_token_type,
67
- exp: token[oauth_tokens_expires_in_column].to_i
68
- }
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
69
88
  end
70
89
 
71
90
  def check_csrf?
@@ -79,24 +98,17 @@ module Rodauth
79
98
 
80
99
  private
81
100
 
82
- def introspection_request(token_type_hint, token)
83
- auth_url = URI(authorization_server_url)
84
- http = Net::HTTP.new(auth_url.host, auth_url.port)
85
- 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]))
86
103
 
87
- request = Net::HTTP::Post.new(auth_url.path + introspect_path)
88
- request["content-type"] = "application/x-www-form-urlencoded"
89
- request["accept"] = json_response_content_type
90
- request.set_form_data({ "token_type_hint" => token_type_hint, "token" => token })
104
+ return require_oauth_application unless token
91
105
 
92
- before_introspection_request(request)
93
- response = http.request(request)
94
- authorization_required unless response.code.to_i == 200
106
+ oauth_application = current_oauth_application
95
107
 
96
- JSON.parse(response.body)
97
- end
108
+ authorization_required unless oauth_application
98
109
 
99
- def before_introspection_request(request); end
110
+ @oauth_application = oauth_application
111
+ end
100
112
 
101
113
  def oauth_server_metadata_body(*)
102
114
  super.tap do |data|
@@ -104,5 +116,24 @@ module Rodauth
104
116
  data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
105
117
  end
106
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
107
138
  end
108
139
  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_token_revocation, :OauthTokenRevocation) do
5
7
  depends :oauth_base
@@ -7,14 +9,10 @@ module Rodauth
7
9
  before "revoke"
8
10
  after "revoke"
9
11
 
10
- notice_flash "The oauth token has been revoked", "revoke_oauth_token"
12
+ notice_flash "The oauth grant has been revoked", "revoke_oauth_grant"
11
13
 
12
14
  # /revoke
13
- route(:revoke) do |r|
14
- next unless is_authorization_server?
15
-
16
- before_revoke_route
17
-
15
+ auth_server_route(:revoke) do |r|
18
16
  if logged_in?
19
17
  require_account
20
18
  require_oauth_application_from_account
@@ -22,24 +20,32 @@ module Rodauth
22
20
  require_oauth_application
23
21
  end
24
22
 
23
+ before_revoke_route
24
+
25
25
  r.post do
26
26
  catch_error do
27
- validate_oauth_revoke_params
27
+ validate_revoke_params
28
28
 
29
- oauth_token = nil
29
+ oauth_grant = nil
30
30
  transaction do
31
31
  before_revoke
32
- oauth_token = revoke_oauth_token
32
+ oauth_grant = revoke_oauth_grant
33
33
  after_revoke
34
34
  end
35
35
 
36
36
  if accepts_json?
37
- json_response_success \
38
- "token" => oauth_token[oauth_tokens_token_column],
39
- "refresh_token" => oauth_token[oauth_tokens_refresh_token_column],
40
- "revoked_at" => convert_timestamp(oauth_token[oauth_tokens_revoked_at_column])
37
+ json_payload = {
38
+ "revoked_at" => convert_timestamp(oauth_grant[oauth_grants_revoked_at_column])
39
+ }
40
+ if param("token_type_hint") == "refresh_token"
41
+ json_payload["refresh_token"] = oauth_grant[oauth_grants_refresh_token_column]
42
+ else
43
+ json_payload["token"] = oauth_grant[oauth_grants_token_column]
44
+ end
45
+
46
+ json_response_success json_payload
41
47
  else
42
- set_notice_flash revoke_oauth_token_notice_flash
48
+ set_notice_flash revoke_oauth_grant_notice_flash
43
49
  redirect request.referer || "/"
44
50
  end
45
51
  end
@@ -48,12 +54,17 @@ module Rodauth
48
54
  end
49
55
  end
50
56
 
51
- def validate_oauth_revoke_params(token_hint_types = %w[access_token refresh_token].freeze)
52
- # check if valid token hint type
53
- if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
54
- redirect_response_error("unsupported_token_type")
57
+ def validate_revoke_params(token_hint_types = %w[access_token refresh_token].freeze)
58
+ token_hint = param_or_nil("token_type_hint")
59
+
60
+ if features.include?(:oauth_jwt) && oauth_jwt_access_tokens && (!token_hint || token_hint == "access_token")
61
+ # JWT access tokens can't be revoked
62
+ throw(:rodauth_error)
55
63
  end
56
64
 
65
+ # check if valid token hint type
66
+ redirect_response_error("unsupported_token_type") if token_hint && !token_hint_types.include?(token_hint)
67
+
57
68
  redirect_response_error("invalid_request") unless param_or_nil("token")
58
69
  end
59
70
 
@@ -68,27 +79,31 @@ module Rodauth
68
79
 
69
80
  private
70
81
 
71
- def revoke_oauth_token
82
+ def revoke_oauth_grant
72
83
  token = param("token")
73
84
 
74
- oauth_token = if param("token_type_hint") == "refresh_token"
75
- oauth_token_by_refresh_token(token)
76
- else
77
- oauth_token_by_token(token)
78
- end
85
+ if param("token_type_hint") == "refresh_token"
86
+ oauth_grant = oauth_grant_by_refresh_token(token)
87
+ token_column = oauth_grants_refresh_token_column
88
+ else
89
+ oauth_grant = oauth_grant_by_token_ds(token).where(
90
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column]
91
+ ).first
92
+ token_column = oauth_grants_token_column
93
+ end
79
94
 
80
- redirect_response_error("invalid_request") unless oauth_token
95
+ redirect_response_error("invalid_request") unless oauth_grant
81
96
 
82
- redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
97
+ redirect_response_error("invalid_request") unless grant_from_application?(oauth_grant, oauth_application)
83
98
 
84
- update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
99
+ update_params = { oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
85
100
 
86
- ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
101
+ ds = db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
87
102
 
88
- oauth_token = __update_and_return__(ds, update_params)
103
+ oauth_grant = __update_and_return__(ds, update_params)
89
104
 
90
- oauth_token[oauth_tokens_token_column] = token
91
- oauth_token
105
+ oauth_grant[token_column] = token
106
+ oauth_grant
92
107
 
93
108
  # If the particular
94
109
  # token is a refresh token and the authorization server supports the