rodauth-oauth 0.10.4 → 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 (65) 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/1_0_0_beta1.md +38 -0
  5. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +4 -6
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  14. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +18 -29
  15. data/lib/rodauth/features/oauth_application_management.rb +59 -72
  16. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  17. data/lib/rodauth/features/oauth_authorization_code_grant.rb +35 -88
  18. data/lib/rodauth/features/oauth_authorize_base.rb +103 -20
  19. data/lib/rodauth/features/oauth_base.rb +365 -302
  20. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  21. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  22. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +46 -28
  23. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  24. data/lib/rodauth/features/oauth_implicit_grant.rb +25 -24
  25. data/lib/rodauth/features/oauth_jwt.rb +52 -688
  26. data/lib/rodauth/features/oauth_jwt_base.rb +435 -0
  27. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +45 -17
  28. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  29. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +62 -0
  30. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  31. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  32. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -21
  33. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  34. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +5 -1
  35. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  36. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  37. data/lib/rodauth/features/oidc.rb +188 -95
  38. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +89 -53
  39. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  40. data/lib/rodauth/oauth/http_extensions.rb +61 -0
  41. data/lib/rodauth/oauth/railtie.rb +20 -0
  42. data/lib/rodauth/oauth/version.rb +1 -1
  43. data/lib/rodauth/oauth.rb +29 -1
  44. data/locales/en.yml +32 -22
  45. data/locales/pt.yml +32 -22
  46. data/templates/authorize.str +19 -24
  47. data/templates/device_search.str +1 -1
  48. data/templates/device_verification.str +2 -2
  49. data/templates/jwks_field.str +1 -0
  50. data/templates/new_oauth_application.str +1 -2
  51. data/templates/oauth_application.str +2 -2
  52. data/templates/oauth_application_oauth_grants.str +54 -0
  53. data/templates/oauth_applications.str +2 -2
  54. data/templates/oauth_grants.str +52 -0
  55. metadata +20 -16
  56. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  57. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  58. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  59. data/lib/rodauth/features/oauth.rb +0 -9
  60. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  61. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  62. data/lib/rodauth/oauth/refinements.rb +0 -48
  63. data/templates/jwt_public_key_field.str +0 -4
  64. data/templates/oauth_application_oauth_tokens.str +0 -52
  65. 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)
@@ -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,23 +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))
70
+ super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
79
71
  end
80
72
 
81
73
  def check_valid_no_fragment_uri?(uri)
@@ -95,9 +87,11 @@ module Rodauth
95
87
  end
96
88
  end
97
89
 
98
- 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)
99
91
  return super unless resource_indicators
100
92
 
93
+ oauth_grant ||= valid_locked_oauth_grant(grant_params)
94
+
101
95
  redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
102
96
 
103
97
  grant_indicators = oauth_grant[oauth_grants_resource_column]
@@ -106,7 +100,15 @@ module Rodauth
106
100
 
107
101
  redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
108
102
 
109
- 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)
110
112
  end
111
113
 
112
114
  def create_oauth_grant(create_params = {})
@@ -117,12 +119,12 @@ module Rodauth
117
119
  end
118
120
 
119
121
  module IndicatorIntrospection
120
- def json_token_introspect_payload(token)
121
- 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]
122
124
 
123
125
  payload = super
124
126
 
125
- token_indicators = token[oauth_tokens_resource_column]
127
+ token_indicators = grant[oauth_grants_resource_column]
126
128
 
127
129
  token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
128
130
 
@@ -134,7 +136,7 @@ module Rodauth
134
136
  def introspection_request(*)
135
137
  payload = super
136
138
 
137
- payload[oauth_tokens_resource_column] = payload["aud"] if payload["aud"]
139
+ payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
138
140
 
139
141
  payload
140
142
  end
@@ -146,6 +148,16 @@ module Rodauth
146
148
 
147
149
  super.merge(aud: resource_indicators)
148
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
149
161
  end
150
162
 
151
163
  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
+ bearer_token = fetch_access_token
20
+
21
+ return unless bearer_token
22
+
23
+ # where in resource server, NOT the authorization server.
24
+ payload = introspection_request("access_token", bearer_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
@@ -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,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
@@ -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,29 +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_ds(token).where(
78
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column]
79
- ).first
80
- 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
81
94
 
82
- redirect_response_error("invalid_request") unless oauth_token
95
+ redirect_response_error("invalid_request") unless oauth_grant
83
96
 
84
- 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)
85
98
 
86
- update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
99
+ update_params = { oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
87
100
 
88
- 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])
89
102
 
90
- oauth_token = __update_and_return__(ds, update_params)
103
+ oauth_grant = __update_and_return__(ds, update_params)
91
104
 
92
- oauth_token[oauth_tokens_token_column] = token
93
- oauth_token
105
+ oauth_grant[token_column] = token
106
+ oauth_grant
94
107
 
95
108
  # If the particular
96
109
  # token is a refresh token and the authorization server supports the