rodauth-oauth 0.7.3 → 0.9.0
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/CHANGELOG.md +1 -418
- data/README.md +30 -390
- data/doc/release_notes/0_0_1.md +3 -0
- data/doc/release_notes/0_0_2.md +15 -0
- data/doc/release_notes/0_0_3.md +31 -0
- data/doc/release_notes/0_0_4.md +36 -0
- data/doc/release_notes/0_0_5.md +36 -0
- data/doc/release_notes/0_0_6.md +21 -0
- data/doc/release_notes/0_1_0.md +44 -0
- data/doc/release_notes/0_2_0.md +43 -0
- data/doc/release_notes/0_3_0.md +28 -0
- data/doc/release_notes/0_4_0.md +18 -0
- data/doc/release_notes/0_4_1.md +9 -0
- data/doc/release_notes/0_4_2.md +5 -0
- data/doc/release_notes/0_4_3.md +3 -0
- data/doc/release_notes/0_5_0.md +11 -0
- data/doc/release_notes/0_5_1.md +13 -0
- data/doc/release_notes/0_6_0.md +9 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_7_0.md +20 -0
- data/doc/release_notes/0_7_1.md +10 -0
- data/doc/release_notes/0_7_2.md +21 -0
- data/doc/release_notes/0_7_3.md +10 -0
- data/doc/release_notes/0_7_4.md +5 -0
- data/doc/release_notes/0_8_0.md +37 -0
- data/doc/release_notes/0_9_0.md +56 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +50 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +55 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +29 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +39 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +30 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +35 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +225 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +252 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +771 -0
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +33 -0
- data/lib/rodauth/features/oauth_device_grant.rb +220 -0
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +252 -0
- data/lib/rodauth/features/oauth_http_mac.rb +3 -21
- data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
- data/lib/rodauth/features/oauth_jwt.rb +276 -100
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
- data/lib/rodauth/features/oauth_management_base.rb +68 -0
- data/lib/rodauth/features/oauth_pkce.rb +98 -0
- data/lib/rodauth/features/oauth_resource_server.rb +21 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
- data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
- data/lib/rodauth/features/oauth_token_management.rb +79 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +36 -6
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +147 -0
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/jwe_extensions.rb +64 -0
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/ttl_store.rb +9 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +33 -12
- data/templates/authorize.str +57 -8
- data/templates/client_secret_field.str +2 -2
- data/templates/description_field.str +1 -1
- data/templates/device_search.str +11 -0
- data/templates/device_verification.str +24 -0
- data/templates/homepage_url_field.str +2 -2
- data/templates/jwks_field.str +4 -0
- data/templates/jwt_public_key_field.str +4 -0
- data/templates/name_field.str +1 -1
- data/templates/new_oauth_application.str +9 -0
- data/templates/oauth_application.str +7 -3
- data/templates/oauth_application_oauth_tokens.str +52 -0
- data/templates/oauth_applications.str +3 -2
- data/templates/oauth_tokens.str +10 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +84 -4
- data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth/refinements"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_pkce, :OauthPkce) do
|
7
|
+
using PrefixExtensions
|
8
|
+
|
9
|
+
depends :oauth_authorization_code_grant
|
10
|
+
|
11
|
+
auth_value_method :use_oauth_pkce?, true
|
12
|
+
|
13
|
+
auth_value_method :oauth_require_pkce, false
|
14
|
+
auth_value_method :oauth_pkce_challenge_method, "S256"
|
15
|
+
|
16
|
+
auth_value_method :oauth_grants_code_challenge_column, :code_challenge
|
17
|
+
auth_value_method :oauth_grants_code_challenge_method_column, :code_challenge_method
|
18
|
+
|
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"
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def authorized_oauth_application?(oauth_application, client_secret, _)
|
27
|
+
return true if use_oauth_pkce? && param_or_nil("code_verifier")
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_oauth_grant_params
|
33
|
+
validate_pkce_challenge_params if use_oauth_pkce?
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_oauth_grant(create_params = {})
|
39
|
+
# PKCE flow
|
40
|
+
if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
|
41
|
+
code_challenge_method = param_or_nil("code_challenge_method")
|
42
|
+
|
43
|
+
create_params[oauth_grants_code_challenge_column] = code_challenge
|
44
|
+
create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
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")
|
54
|
+
|
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
|
59
|
+
end
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_pkce_challenge_params
|
65
|
+
if param_or_nil("code_challenge")
|
66
|
+
|
67
|
+
challenge_method = param_or_nil("code_challenge_method")
|
68
|
+
redirect_response_error("code_challenge_required") unless oauth_pkce_challenge_method == challenge_method
|
69
|
+
else
|
70
|
+
return unless oauth_require_pkce
|
71
|
+
|
72
|
+
redirect_response_error("code_challenge_required")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_valid_grant_challenge?(grant, verifier)
|
77
|
+
challenge = grant[oauth_grants_code_challenge_column]
|
78
|
+
|
79
|
+
case grant[oauth_grants_code_challenge_method_column]
|
80
|
+
when "plain"
|
81
|
+
challenge == verifier
|
82
|
+
when "S256"
|
83
|
+
generated_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier))
|
84
|
+
generated_challenge.delete_suffix!("=") while generated_challenge.end_with?("=")
|
85
|
+
|
86
|
+
challenge == generated_challenge
|
87
|
+
else
|
88
|
+
redirect_response_error("unsupported_transform_algorithm")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def oauth_server_metadata_body(*)
|
93
|
+
super.tap do |data|
|
94
|
+
data[:code_challenge_methods_supported] = oauth_pkce_challenge_method if use_oauth_pkce?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_resource_server, :OauthResourceServer) do
|
5
|
+
def authorization_token
|
6
|
+
return @authorization_token if defined?(@authorization_token)
|
7
|
+
|
8
|
+
# check if there is a token
|
9
|
+
bearer_token = fetch_access_token
|
10
|
+
|
11
|
+
return unless bearer_token
|
12
|
+
|
13
|
+
# where in resource server, NOT the authorization server.
|
14
|
+
payload = introspection_request("access_token", bearer_token)
|
15
|
+
|
16
|
+
return unless payload["active"]
|
17
|
+
|
18
|
+
@authorization_token = payload
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "onelogin/ruby-saml"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oauth_saml_bearer_grant, :OauthSamlBearerGrant) do
|
7
|
+
depends :oauth_assertion_base
|
8
|
+
|
9
|
+
auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
|
10
|
+
auth_value_method :oauth_saml_cert, nil
|
11
|
+
auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
|
12
|
+
auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
13
|
+
|
14
|
+
auth_value_method :oauth_saml_security_authn_requests_signed, true
|
15
|
+
auth_value_method :oauth_saml_security_metadata_signed, true
|
16
|
+
auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
|
17
|
+
auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
|
18
|
+
|
19
|
+
auth_value_methods(
|
20
|
+
:require_oauth_application_from_saml2_bearer_assertion_issuer,
|
21
|
+
:require_oauth_application_from_saml2_bearer_assertion_subject,
|
22
|
+
:account_from_saml2_bearer_assertion
|
23
|
+
)
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def require_oauth_application_from_saml2_bearer_assertion_issuer(assertion)
|
28
|
+
saml = saml_assertion(assertion)
|
29
|
+
|
30
|
+
return unless saml
|
31
|
+
|
32
|
+
db[oauth_applications_table].where(
|
33
|
+
oauth_applications_homepage_url_column => saml.issuers
|
34
|
+
).first
|
35
|
+
end
|
36
|
+
|
37
|
+
def require_oauth_application_from_saml2_bearer_assertion_subject(assertion)
|
38
|
+
saml = saml_assertion(assertion)
|
39
|
+
|
40
|
+
return unless saml
|
41
|
+
|
42
|
+
db[oauth_applications_table].where(
|
43
|
+
oauth_applications_client_id_column => saml.nameid
|
44
|
+
).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def account_from_saml2_bearer_assertion(assertion)
|
48
|
+
saml = saml_assertion(assertion)
|
49
|
+
|
50
|
+
return unless saml
|
51
|
+
|
52
|
+
account_from_bearer_assertion_subject(saml.nameid)
|
53
|
+
end
|
54
|
+
|
55
|
+
def saml_assertion(assertion)
|
56
|
+
settings = OneLogin::RubySaml::Settings.new
|
57
|
+
settings.idp_cert = oauth_saml_cert
|
58
|
+
settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
|
59
|
+
settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
|
60
|
+
settings.name_identifier_format = oauth_saml_name_identifier_format
|
61
|
+
settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
|
62
|
+
settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
|
63
|
+
settings.security[:digest_method] = oauth_saml_security_digest_method
|
64
|
+
settings.security[:signature_method] = oauth_saml_security_signature_method
|
65
|
+
|
66
|
+
response = OneLogin::RubySaml::Response.new(assertion, settings: settings, skip_recipient_check: true)
|
67
|
+
|
68
|
+
# 3. he Assertion MUST have an expiry that limits the time window ...
|
69
|
+
# 4. The Assertion MUST have an expiry that limits the time window ...
|
70
|
+
# 5. The <Subject> element MUST contain at least one ...
|
71
|
+
# 6. The authorization server MUST reject the entire Assertion if the ...
|
72
|
+
# 7. If the Assertion issuer directly authenticated the subject, ...
|
73
|
+
redirect_response_error("invalid_grant") unless response.is_valid?
|
74
|
+
|
75
|
+
# In order to issue an access token response as described in OAuth 2.0
|
76
|
+
# [RFC6749] or to rely on an Assertion for client authentication, the
|
77
|
+
# authorization server MUST validate the Assertion according to the
|
78
|
+
# criteria below.
|
79
|
+
|
80
|
+
# 1. The Assertion's <Issuer> element MUST contain a unique identifier
|
81
|
+
# for the entity that issued the Assertion.
|
82
|
+
redirect_response_error("invalid_grant") unless response.issuers.size == 1
|
83
|
+
|
84
|
+
# 2. in addition to the URI references
|
85
|
+
# discussed there, the token endpoint URL of the authorization
|
86
|
+
# server MAY be used as a URI that identifies the authorization
|
87
|
+
# server as an intended audience. The authorization server MUST
|
88
|
+
# reject any Assertion that does not contain its own identity as
|
89
|
+
# the intended audience.
|
90
|
+
redirect_response_error("invalid_grant") if response.audiences && !response.audiences.include?(token_url)
|
91
|
+
|
92
|
+
response
|
93
|
+
end
|
94
|
+
|
95
|
+
def oauth_server_metadata_body(*)
|
96
|
+
super.tap do |data|
|
97
|
+
data[:grant_types_supported] << "urn:ietf:params:oauth:grant-type:saml2-bearer"
|
98
|
+
data[:token_endpoint_auth_methods_supported] << "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_token_introspection, :OauthTokenIntrospection) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
before "introspect"
|
8
|
+
|
9
|
+
auth_value_methods(
|
10
|
+
:before_introspection_request
|
11
|
+
)
|
12
|
+
|
13
|
+
# /introspect
|
14
|
+
route(:introspect) do |r|
|
15
|
+
next unless is_authorization_server?
|
16
|
+
|
17
|
+
before_introspect_route
|
18
|
+
|
19
|
+
r.post do
|
20
|
+
catch_error do
|
21
|
+
validate_oauth_introspect_params
|
22
|
+
|
23
|
+
before_introspect
|
24
|
+
oauth_token = case param("token_type_hint")
|
25
|
+
when "access_token"
|
26
|
+
oauth_token_by_token(param("token"))
|
27
|
+
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"))
|
31
|
+
end
|
32
|
+
|
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
|
+
|
40
|
+
json_response_success(json_token_introspect_payload(oauth_token))
|
41
|
+
end
|
42
|
+
|
43
|
+
throw_json_response_error(invalid_oauth_response_status, "invalid_request")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Token introspect
|
48
|
+
|
49
|
+
def validate_oauth_introspect_params(token_hint_types = %w[access_token refresh_token].freeze)
|
50
|
+
# check if valid token hint type
|
51
|
+
if param_or_nil("token_type_hint") && !token_hint_types.include?(param("token_type_hint"))
|
52
|
+
redirect_response_error("unsupported_token_type")
|
53
|
+
end
|
54
|
+
|
55
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
56
|
+
end
|
57
|
+
|
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
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def check_csrf?
|
72
|
+
case request.path
|
73
|
+
when introspect_path
|
74
|
+
false
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
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"
|
86
|
+
|
87
|
+
request = Net::HTTP::Post.new(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 })
|
91
|
+
|
92
|
+
before_introspection_request(request)
|
93
|
+
response = http.request(request)
|
94
|
+
authorization_required unless response.code.to_i == 200
|
95
|
+
|
96
|
+
JSON.parse(response.body)
|
97
|
+
end
|
98
|
+
|
99
|
+
def before_introspection_request(request); end
|
100
|
+
|
101
|
+
def oauth_server_metadata_body(*)
|
102
|
+
super.tap do |data|
|
103
|
+
data[:introspection_endpoint] = introspect_url
|
104
|
+
data[:introspection_endpoint_auth_methods_supported] = %w[client_secret_basic]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_token_management, :OauthTokenManagement) do
|
5
|
+
using RegexpExtensions
|
6
|
+
|
7
|
+
depends :oauth_management_base
|
8
|
+
|
9
|
+
view "oauth_tokens", "My Oauth Tokens", "oauth_tokens"
|
10
|
+
|
11
|
+
button "Revoke", "oauth_token_revoke"
|
12
|
+
|
13
|
+
auth_value_method :oauth_tokens_path, "oauth-tokens"
|
14
|
+
|
15
|
+
%w[token refresh_token expires_in revoked_at].each do |param|
|
16
|
+
translatable_method :"oauth_tokens_#{param}_label", param.gsub("_", " ").capitalize
|
17
|
+
end
|
18
|
+
|
19
|
+
auth_value_method :oauth_tokens_route, "oauth-tokens"
|
20
|
+
auth_value_method :oauth_tokens_id_pattern, Integer
|
21
|
+
auth_value_method :oauth_tokens_per_page, 20
|
22
|
+
|
23
|
+
auth_value_methods(
|
24
|
+
:oauth_token_path
|
25
|
+
)
|
26
|
+
|
27
|
+
def oauth_tokens_path(opts = {})
|
28
|
+
route_path(oauth_tokens_route, opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
def oauth_tokens_url(opts = {})
|
32
|
+
route_url(oauth_tokens_route, opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
def oauth_token_path(id)
|
36
|
+
"#{oauth_tokens_path}/#{id}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def oauth_tokens
|
40
|
+
request.on(oauth_tokens_route) do
|
41
|
+
require_account
|
42
|
+
|
43
|
+
request.get do
|
44
|
+
page = Integer(param_or_nil("page") || 1)
|
45
|
+
per_page = per_page_param(oauth_tokens_per_page)
|
46
|
+
|
47
|
+
scope.instance_variable_set(:@oauth_tokens, db[oauth_tokens_table]
|
48
|
+
.select(Sequel[oauth_tokens_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
|
49
|
+
.join(oauth_applications_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
|
50
|
+
Sequel[oauth_applications_table][oauth_applications_id_column])
|
51
|
+
.where(Sequel[oauth_tokens_table][oauth_tokens_account_id_column] => account_id)
|
52
|
+
.where(oauth_tokens_revoked_at_column => nil)
|
53
|
+
.order(Sequel.desc(oauth_tokens_id_column))
|
54
|
+
.paginate(page, per_page))
|
55
|
+
oauth_tokens_view
|
56
|
+
end
|
57
|
+
|
58
|
+
request.post(oauth_tokens_id_pattern) do |id|
|
59
|
+
db[oauth_tokens_table]
|
60
|
+
.where(oauth_tokens_id_column => id)
|
61
|
+
.where(oauth_tokens_account_id_column => account_id)
|
62
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
63
|
+
|
64
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
65
|
+
redirect oauth_tokens_path || "/"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_csrf?
|
71
|
+
case request.path
|
72
|
+
when oauth_tokens_path
|
73
|
+
only_json? ? false : super
|
74
|
+
else
|
75
|
+
super
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_token_revocation, :OauthTokenRevocation) do
|
5
|
+
depends :oauth_base
|
6
|
+
|
7
|
+
before "revoke"
|
8
|
+
after "revoke"
|
9
|
+
|
10
|
+
notice_flash "The oauth token has been revoked", "revoke_oauth_token"
|
11
|
+
|
12
|
+
# /revoke
|
13
|
+
route(:revoke) do |r|
|
14
|
+
next unless is_authorization_server?
|
15
|
+
|
16
|
+
before_revoke_route
|
17
|
+
|
18
|
+
if logged_in?
|
19
|
+
require_account
|
20
|
+
require_oauth_application_from_account
|
21
|
+
else
|
22
|
+
require_oauth_application
|
23
|
+
end
|
24
|
+
|
25
|
+
r.post do
|
26
|
+
catch_error do
|
27
|
+
validate_oauth_revoke_params
|
28
|
+
|
29
|
+
oauth_token = nil
|
30
|
+
transaction do
|
31
|
+
before_revoke
|
32
|
+
oauth_token = revoke_oauth_token
|
33
|
+
after_revoke
|
34
|
+
end
|
35
|
+
|
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])
|
41
|
+
else
|
42
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
43
|
+
redirect request.referer || "/"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
redirect_response_error("invalid_request", request.referer || "/")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
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")
|
55
|
+
end
|
56
|
+
|
57
|
+
redirect_response_error("invalid_request") unless param_or_nil("token")
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_csrf?
|
61
|
+
case request.path
|
62
|
+
when revoke_path
|
63
|
+
!json_request?
|
64
|
+
else
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def revoke_oauth_token
|
72
|
+
token = param("token")
|
73
|
+
|
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
|
79
|
+
|
80
|
+
redirect_response_error("invalid_request") unless oauth_token
|
81
|
+
|
82
|
+
redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
|
83
|
+
|
84
|
+
update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
|
85
|
+
|
86
|
+
ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
|
87
|
+
|
88
|
+
oauth_token = __update_and_return__(ds, update_params)
|
89
|
+
|
90
|
+
oauth_token[oauth_tokens_token_column] = token
|
91
|
+
oauth_token
|
92
|
+
|
93
|
+
# If the particular
|
94
|
+
# token is a refresh token and the authorization server supports the
|
95
|
+
# revocation of access tokens, then the authorization server SHOULD
|
96
|
+
# also invalidate all access tokens based on the same authorization
|
97
|
+
# grant
|
98
|
+
#
|
99
|
+
# we don't need to do anything here, as we revalidate existing tokens
|
100
|
+
end
|
101
|
+
|
102
|
+
def oauth_server_metadata_body(*)
|
103
|
+
super.tap do |data|
|
104
|
+
data[:revocation_endpoint] = revoke_url
|
105
|
+
data[:revocation_endpoint_auth_methods_supported] = nil # because it's client_secret_basic
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -65,6 +65,13 @@ module Rodauth
|
|
65
65
|
auth_value_method :oauth_application_default_scope, "openid"
|
66
66
|
auth_value_method :oauth_application_scopes, %w[openid]
|
67
67
|
|
68
|
+
auth_value_method :oauth_applications_id_token_signed_response_alg_column, :id_token_signed_response_alg
|
69
|
+
auth_value_method :oauth_applications_id_token_encrypted_response_alg_column, :id_token_encrypted_response_alg
|
70
|
+
auth_value_method :oauth_applications_id_token_encrypted_response_enc_column, :id_token_encrypted_response_enc
|
71
|
+
auth_value_method :oauth_applications_userinfo_signed_response_alg_column, :userinfo_signed_response_alg
|
72
|
+
auth_value_method :oauth_applications_userinfo_encrypted_response_alg_column, :userinfo_encrypted_response_alg
|
73
|
+
auth_value_method :oauth_applications_userinfo_encrypted_response_enc_column, :userinfo_encrypted_response_enc
|
74
|
+
|
68
75
|
auth_value_method :oauth_grants_nonce_column, :nonce
|
69
76
|
auth_value_method :oauth_tokens_nonce_column, :nonce
|
70
77
|
|
@@ -106,7 +113,23 @@ module Rodauth
|
|
106
113
|
|
107
114
|
fill_with_account_claims(oidc_claims, account, oauth_scopes)
|
108
115
|
|
109
|
-
|
116
|
+
@oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
|
117
|
+
|
118
|
+
if (algo = @oauth_application && @oauth_application[oauth_applications_userinfo_signed_response_alg_column])
|
119
|
+
params = {
|
120
|
+
jwks: oauth_application_jwks,
|
121
|
+
encryption_algorithm: @oauth_application[oauth_applications_userinfo_encrypted_response_alg_column],
|
122
|
+
encryption_method: @oauth_application[oauth_applications_userinfo_encrypted_response_enc_column]
|
123
|
+
}
|
124
|
+
jwt = jwt_encode(
|
125
|
+
oidc_claims,
|
126
|
+
signing_algorithm: algo,
|
127
|
+
**params
|
128
|
+
)
|
129
|
+
jwt_response_success(jwt)
|
130
|
+
else
|
131
|
+
json_response_success(oidc_claims)
|
132
|
+
end
|
110
133
|
end
|
111
134
|
|
112
135
|
throw_json_response_error(authorization_required_error_status, "invalid_token")
|
@@ -283,7 +306,7 @@ module Rodauth
|
|
283
306
|
redirect_response_error("consent_required")
|
284
307
|
end
|
285
308
|
when "select-account"
|
286
|
-
#
|
309
|
+
# only works if select_account plugin is available
|
287
310
|
require_select_account if respond_to?(:require_select_account)
|
288
311
|
else
|
289
312
|
redirect_response_error("invalid_request")
|
@@ -302,7 +325,7 @@ module Rodauth
|
|
302
325
|
super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
|
303
326
|
end
|
304
327
|
|
305
|
-
def create_oauth_token
|
328
|
+
def create_oauth_token(*)
|
306
329
|
oauth_token = super
|
307
330
|
generate_id_token(oauth_token)
|
308
331
|
oauth_token
|
@@ -330,7 +353,13 @@ module Rodauth
|
|
330
353
|
|
331
354
|
fill_with_account_claims(id_token_claims, account, oauth_scopes)
|
332
355
|
|
333
|
-
|
356
|
+
params = {
|
357
|
+
jwks: oauth_application_jwks,
|
358
|
+
signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm,
|
359
|
+
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
|
360
|
+
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
361
|
+
}
|
362
|
+
oauth_token[:id_token] = jwt_encode(id_token_claims, **params)
|
334
363
|
end
|
335
364
|
|
336
365
|
# aka fill_with_standard_claims
|
@@ -443,7 +472,7 @@ module Rodauth
|
|
443
472
|
|
444
473
|
# Metadata
|
445
474
|
|
446
|
-
def openid_configuration_body(path)
|
475
|
+
def openid_configuration_body(path = nil)
|
447
476
|
metadata = oauth_server_metadata_body(path).select do |k, _|
|
448
477
|
VALID_METADATA_KEYS.include?(k)
|
449
478
|
end
|
@@ -461,7 +490,8 @@ module Rodauth
|
|
461
490
|
scope_claims.unshift("auth_time") if last_account_login_at
|
462
491
|
|
463
492
|
response_types_supported = metadata[:response_types_supported]
|
464
|
-
|
493
|
+
|
494
|
+
if metadata[:grant_types_supported].include?("implicit")
|
465
495
|
response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
|
466
496
|
end
|
467
497
|
|