rodauth-oauth 0.10.3 → 1.0.0.pre.beta1

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