rodauth-oauth 0.10.4 → 1.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
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