rodauth-oauth 0.10.4 → 1.0.0.pre.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIGRATION-GUIDE-v1.md +286 -0
- data/README.md +28 -35
- data/doc/release_notes/1_0_0_beta1.md +38 -0
- data/doc/release_notes/1_0_0_beta2.md +34 -0
- data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
- data/lib/rodauth/features/oauth_application_management.rb +61 -74
- data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
- data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
- data/lib/rodauth/features/oauth_base.rb +397 -315
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
- data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
- data/lib/rodauth/features/oauth_grant_management.rb +70 -0
- data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
- data/lib/rodauth/features/oauth_jwt.rb +53 -689
- data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
- data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
- data/lib/rodauth/features/oauth_management_base.rb +2 -0
- data/lib/rodauth/features/oauth_pkce.rb +22 -26
- data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
- data/lib/rodauth/features/oauth_resource_server.rb +59 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
- data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
- data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
- data/lib/rodauth/features/oidc.rb +382 -241
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
- data/lib/rodauth/oauth/database_extensions.rb +8 -6
- data/lib/rodauth/oauth/http_extensions.rb +74 -0
- data/lib/rodauth/oauth/railtie.rb +20 -0
- data/lib/rodauth/oauth/ttl_store.rb +2 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/lib/rodauth/oauth.rb +29 -1
- data/locales/en.yml +34 -22
- data/locales/pt.yml +34 -22
- data/templates/authorize.str +19 -17
- data/templates/device_search.str +1 -1
- data/templates/device_verification.str +2 -2
- data/templates/jwks_field.str +1 -0
- data/templates/new_oauth_application.str +1 -2
- data/templates/oauth_application.str +2 -2
- data/templates/oauth_application_oauth_grants.str +54 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_grants.str +52 -0
- metadata +23 -16
- data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
- data/lib/rodauth/features/oauth.rb +0 -9
- data/lib/rodauth/features/oauth_http_mac.rb +0 -86
- data/lib/rodauth/features/oauth_token_management.rb +0 -81
- data/lib/rodauth/oauth/refinements.rb +0 -48
- data/templates/jwt_public_key_field.str +0 -4
- data/templates/oauth_application_oauth_tokens.str +0 -52
- data/templates/oauth_tokens.str +0 -50
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_jwt_secured_authorization_request, :OauthJwtSecuredAuthorizationRequest) do
|
7
|
+
ALLOWED_REQUEST_URI_CONTENT_TYPES = %w[application/jose application/oauth-authz-req+jwt].freeze
|
8
|
+
|
9
|
+
depends :oauth_authorize_base, :oauth_jwt_base
|
10
|
+
|
11
|
+
auth_value_method :oauth_require_request_uri_registration, false
|
12
|
+
auth_value_method :oauth_request_object_signing_alg_allow_none, false
|
13
|
+
|
14
|
+
auth_value_method :oauth_applications_request_uris_column, :request_uris
|
15
|
+
|
16
|
+
auth_value_method :oauth_applications_request_object_signing_alg_column, :request_object_signing_alg
|
17
|
+
auth_value_method :oauth_applications_request_object_encryption_alg_column, :request_object_encryption_alg
|
18
|
+
auth_value_method :oauth_applications_request_object_encryption_enc_column, :request_object_encryption_enc
|
19
|
+
|
20
|
+
translatable_method :oauth_invalid_request_object_message, "request object is invalid"
|
21
|
+
|
22
|
+
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# /authorize
|
27
|
+
|
28
|
+
def validate_authorize_params
|
29
|
+
request_object = param_or_nil("request")
|
30
|
+
|
31
|
+
request_uri = param_or_nil("request_uri")
|
32
|
+
|
33
|
+
return super unless (request_object || request_uri) && oauth_application
|
34
|
+
|
35
|
+
if request_uri
|
36
|
+
request_uri = CGI.unescape(request_uri)
|
37
|
+
|
38
|
+
redirect_response_error("invalid_request_uri") unless supported_request_uri?(request_uri, oauth_application)
|
39
|
+
|
40
|
+
response = http_request(request_uri)
|
41
|
+
|
42
|
+
unless response.code.to_i == 200 && ALLOWED_REQUEST_URI_CONTENT_TYPES.include?(response["content-type"])
|
43
|
+
redirect_response_error("invalid_request_uri")
|
44
|
+
end
|
45
|
+
|
46
|
+
request_object = response.body
|
47
|
+
end
|
48
|
+
|
49
|
+
request_sig_enc_opts = {
|
50
|
+
jws_algorithm: oauth_application[oauth_applications_request_object_signing_alg_column],
|
51
|
+
jws_encryption_algorithm: oauth_application[oauth_applications_request_object_encryption_alg_column],
|
52
|
+
jws_encryption_method: oauth_application[oauth_applications_request_object_encryption_enc_column]
|
53
|
+
}.compact
|
54
|
+
|
55
|
+
request_sig_enc_opts[:jws_algorithm] ||= "none" if oauth_request_object_signing_alg_allow_none
|
56
|
+
|
57
|
+
if request_sig_enc_opts[:jws_algorithm] == "none"
|
58
|
+
jwks = nil
|
59
|
+
elsif (jwks = oauth_application_jwks(oauth_application))
|
60
|
+
jwks = JSON.parse(jwks, symbolize_names: true) if jwks.is_a?(String)
|
61
|
+
else
|
62
|
+
redirect_response_error("invalid_request_object")
|
63
|
+
end
|
64
|
+
|
65
|
+
claims = jwt_decode(request_object,
|
66
|
+
jwks: jwks,
|
67
|
+
verify_jti: false,
|
68
|
+
verify_iss: false,
|
69
|
+
verify_aud: false,
|
70
|
+
**request_sig_enc_opts)
|
71
|
+
|
72
|
+
redirect_response_error("invalid_request_object") unless claims
|
73
|
+
|
74
|
+
if (iss = claims["iss"]) && (iss != oauth_application[oauth_applications_client_id_column])
|
75
|
+
redirect_response_error("invalid_request_object")
|
76
|
+
end
|
77
|
+
|
78
|
+
if (aud = claims["aud"]) && !verify_aud(aud, oauth_jwt_issuer)
|
79
|
+
redirect_response_error("invalid_request_object")
|
80
|
+
end
|
81
|
+
|
82
|
+
# If signed, the Authorization Request
|
83
|
+
# Object SHOULD contain the Claims "iss" (issuer) and "aud" (audience)
|
84
|
+
# as members, with their semantics being the same as defined in the JWT
|
85
|
+
# [RFC7519] specification. The value of "aud" should be the value of
|
86
|
+
# the Authorization Server (AS) "issuer" as defined in RFC8414
|
87
|
+
# [RFC8414].
|
88
|
+
claims.delete("iss")
|
89
|
+
audience = claims.delete("aud")
|
90
|
+
|
91
|
+
redirect_response_error("invalid_request_object") if audience && audience != oauth_jwt_issuer
|
92
|
+
|
93
|
+
claims.each do |k, v|
|
94
|
+
request.params[k.to_s] = v
|
95
|
+
end
|
96
|
+
|
97
|
+
super
|
98
|
+
end
|
99
|
+
|
100
|
+
def supported_request_uri?(request_uri, oauth_application)
|
101
|
+
return false unless check_valid_uri?(request_uri)
|
102
|
+
|
103
|
+
request_uris = oauth_application[oauth_applications_request_uris_column]
|
104
|
+
|
105
|
+
request_uris.nil? || request_uris.split(oauth_scope_separator).one? { |uri| request_uri.start_with?(uri) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def oauth_server_metadata_body(*)
|
109
|
+
super.tap do |data|
|
110
|
+
data[:request_parameter_supported] = true
|
111
|
+
data[:request_uri_parameter_supported] = true
|
112
|
+
data[:require_request_uri_registration] = oauth_require_request_uri_registration
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -1,44 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "rodauth/oauth
|
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 :
|
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 :
|
20
|
-
translatable_method :
|
21
|
-
auth_value_method :
|
22
|
-
translatable_method :
|
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
|
27
|
-
return
|
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
|
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
|
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
|
51
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
51
|
+
def validate_token_params
|
53
52
|
super
|
54
53
|
|
55
54
|
return unless resource_indicators
|
@@ -59,27 +58,16 @@ module Rodauth
|
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def
|
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(
|
79
|
-
end
|
80
|
-
|
81
|
-
def check_valid_no_fragment_uri?(uri)
|
82
|
-
check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
|
70
|
+
super(oauth_grant, update_params.merge(oauth_grants_resource_column => resource_indicators))
|
83
71
|
end
|
84
72
|
|
85
73
|
module IndicatorAuthorizationCodeGrant
|
@@ -95,9 +83,11 @@ module Rodauth
|
|
95
83
|
end
|
96
84
|
end
|
97
85
|
|
98
|
-
def
|
86
|
+
def create_token_from_authorization_code(grant_params, *args, oauth_grant: nil)
|
99
87
|
return super unless resource_indicators
|
100
88
|
|
89
|
+
oauth_grant ||= valid_locked_oauth_grant(grant_params)
|
90
|
+
|
101
91
|
redirect_response_error("invalid_target") unless oauth_grant[oauth_grants_resource_column]
|
102
92
|
|
103
93
|
grant_indicators = oauth_grant[oauth_grants_resource_column]
|
@@ -106,7 +96,15 @@ module Rodauth
|
|
106
96
|
|
107
97
|
redirect_response_error("invalid_target") unless (grant_indicators - resource_indicators) != grant_indicators
|
108
98
|
|
109
|
-
|
99
|
+
# update ownership
|
100
|
+
if grant_indicators != resource_indicators
|
101
|
+
oauth_grant = __update_and_return__(
|
102
|
+
db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column]),
|
103
|
+
oauth_grants_resource_column => resource_indicators
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
super({ oauth_grants_id_column => oauth_grant[oauth_grants_id_column] }, *args, oauth_grant: oauth_grant)
|
110
108
|
end
|
111
109
|
|
112
110
|
def create_oauth_grant(create_params = {})
|
@@ -117,12 +115,12 @@ module Rodauth
|
|
117
115
|
end
|
118
116
|
|
119
117
|
module IndicatorIntrospection
|
120
|
-
def json_token_introspect_payload(
|
121
|
-
return super unless
|
118
|
+
def json_token_introspect_payload(grant)
|
119
|
+
return super unless grant[oauth_grants_id_column]
|
122
120
|
|
123
121
|
payload = super
|
124
122
|
|
125
|
-
token_indicators =
|
123
|
+
token_indicators = grant[oauth_grants_resource_column]
|
126
124
|
|
127
125
|
token_indicators = token_indicators.split(" ") if token_indicators.is_a?(String)
|
128
126
|
|
@@ -134,7 +132,7 @@ module Rodauth
|
|
134
132
|
def introspection_request(*)
|
135
133
|
payload = super
|
136
134
|
|
137
|
-
payload[
|
135
|
+
payload[oauth_grants_resource_column] = payload["aud"] if payload["aud"]
|
138
136
|
|
139
137
|
payload
|
140
138
|
end
|
@@ -146,6 +144,16 @@ module Rodauth
|
|
146
144
|
|
147
145
|
super.merge(aud: resource_indicators)
|
148
146
|
end
|
147
|
+
|
148
|
+
def jwt_decode(token, verify_aud: true, **args)
|
149
|
+
claims = super(token, verify_aud: false, **args)
|
150
|
+
|
151
|
+
return claims unless verify_aud
|
152
|
+
|
153
|
+
return unless claims["aud"] && claims["aud"].one? { |aud| request.url.starts_with?(aud) }
|
154
|
+
|
155
|
+
claims
|
156
|
+
end
|
149
157
|
end
|
150
158
|
|
151
159
|
def self.included(rodauth)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_resource_server, :OauthResourceServer) do
|
7
|
+
depends :oauth_token_introspection
|
8
|
+
|
9
|
+
auth_value_method :is_authorization_server?, false
|
10
|
+
|
11
|
+
auth_value_methods(
|
12
|
+
:before_introspection_request
|
13
|
+
)
|
14
|
+
|
15
|
+
def authorization_token
|
16
|
+
return @authorization_token if defined?(@authorization_token)
|
17
|
+
|
18
|
+
# check if there is a token
|
19
|
+
access_token = fetch_access_token
|
20
|
+
|
21
|
+
return unless access_token
|
22
|
+
|
23
|
+
# where in resource server, NOT the authorization server.
|
24
|
+
payload = introspection_request("access_token", access_token)
|
25
|
+
|
26
|
+
return unless payload["active"]
|
27
|
+
|
28
|
+
@authorization_token = payload
|
29
|
+
end
|
30
|
+
|
31
|
+
def require_oauth_authorization(*scopes)
|
32
|
+
authorization_required unless authorization_token
|
33
|
+
|
34
|
+
aux_scopes = authorization_token["scope"]
|
35
|
+
|
36
|
+
token_scopes = if aux_scopes
|
37
|
+
aux_scopes.split(oauth_scope_separator)
|
38
|
+
else
|
39
|
+
[]
|
40
|
+
end
|
41
|
+
|
42
|
+
authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def introspection_request(token_type_hint, token)
|
48
|
+
introspect_url = URI("#{authorization_server_url}#{introspect_path}")
|
49
|
+
|
50
|
+
response = http_request(introspect_url, { "token_type_hint" => token_type_hint, "token" => token }) do |request|
|
51
|
+
before_introspection_request(request)
|
52
|
+
end
|
53
|
+
|
54
|
+
JSON.parse(response.body)
|
55
|
+
end
|
56
|
+
|
57
|
+
def before_introspection_request(request); end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "onelogin/ruby-saml"
|
4
|
+
require "rodauth/oauth"
|
4
5
|
|
5
6
|
module Rodauth
|
6
7
|
Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
|
@@ -16,12 +17,18 @@ module Rodauth
|
|
16
17
|
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
|
17
18
|
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
|
18
19
|
|
20
|
+
auth_value_method :max_param_bytesize, nil if Rodauth::VERSION >= "2.26.0"
|
21
|
+
|
19
22
|
auth_value_methods(
|
20
23
|
:require_oauth_application_from_saml2_bearer_assertion_issuer,
|
21
24
|
:require_oauth_application_from_saml2_bearer_assertion_subject,
|
22
25
|
:account_from_saml2_bearer_assertion
|
23
26
|
)
|
24
27
|
|
28
|
+
def oauth_grant_types_supported
|
29
|
+
super | %w[urn:ietf:params:oauth:grant-type:saml2-bearer]
|
30
|
+
end
|
31
|
+
|
25
32
|
private
|
26
33
|
|
27
34
|
def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
|
@@ -94,7 +101,6 @@ module Rodauth
|
|
94
101
|
|
95
102
|
def oauth_server_metadata_body(*)
|
96
103
|
super.tap do |data|
|
97
|
-
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:saml2-bearer"
|
98
104
|
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
|
99
105
|
end
|
100
106
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rodauth/oauth"
|
4
|
+
require "rodauth/oauth/http_extensions"
|
5
|
+
|
3
6
|
module Rodauth
|
4
7
|
Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
|
5
8
|
depends :oauth_base
|
@@ -7,47 +10,44 @@ module Rodauth
|
|
7
10
|
before "introspect"
|
8
11
|
|
9
12
|
auth_value_methods(
|
10
|
-
:
|
13
|
+
:resource_owner_identifier
|
11
14
|
)
|
12
15
|
|
13
16
|
# /introspect
|
14
|
-
|
15
|
-
|
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
|
-
|
23
|
+
validate_introspect_params
|
24
|
+
|
25
|
+
token_type_hint = param_or_nil("token_type_hint")
|
23
26
|
|
24
27
|
before_introspect
|
25
|
-
|
26
|
-
when "access_token"
|
27
|
-
|
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
|
-
|
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
|
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(
|
41
|
+
json_response_success(json_token_introspect_payload(oauth_grant))
|
42
42
|
end
|
43
43
|
|
44
|
-
throw_json_response_error(
|
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
|
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(
|
60
|
-
return { active: false } unless
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
84
|
-
|
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
|
-
|
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
|
-
|
94
|
-
response = http.request(request)
|
95
|
-
authorization_required unless response.code.to_i == 200
|
106
|
+
oauth_application = current_oauth_application
|
96
107
|
|
97
|
-
|
98
|
-
end
|
108
|
+
authorization_required unless oauth_application
|
99
109
|
|
100
|
-
|
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
|