rodauth-oauth 0.10.4 → 1.0.0.pre.beta2
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.
- 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
|