rodauth-oauth 0.7.4 → 0.9.1
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 -424
- 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/doc/release_notes/0_9_1.md +9 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +25 -4
- 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 +27 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +17 -5
- 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 +6 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +12 -15
- 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 +778 -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 +275 -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 +38 -9
- 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 +80 -3
- 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")
|
@@ -211,8 +234,7 @@ module Rodauth
|
|
211
234
|
href: authorization_server_url
|
212
235
|
}]
|
213
236
|
})
|
214
|
-
|
215
|
-
request.halt
|
237
|
+
return_response(json_payload)
|
216
238
|
end
|
217
239
|
end
|
218
240
|
end
|
@@ -283,7 +305,7 @@ module Rodauth
|
|
283
305
|
redirect_response_error("consent_required")
|
284
306
|
end
|
285
307
|
when "select-account"
|
286
|
-
#
|
308
|
+
# only works if select_account plugin is available
|
287
309
|
require_select_account if respond_to?(:require_select_account)
|
288
310
|
else
|
289
311
|
redirect_response_error("invalid_request")
|
@@ -302,7 +324,7 @@ module Rodauth
|
|
302
324
|
super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
|
303
325
|
end
|
304
326
|
|
305
|
-
def create_oauth_token
|
327
|
+
def create_oauth_token(*)
|
306
328
|
oauth_token = super
|
307
329
|
generate_id_token(oauth_token)
|
308
330
|
oauth_token
|
@@ -330,7 +352,13 @@ module Rodauth
|
|
330
352
|
|
331
353
|
fill_with_account_claims(id_token_claims, account, oauth_scopes)
|
332
354
|
|
333
|
-
|
355
|
+
params = {
|
356
|
+
jwks: oauth_application_jwks,
|
357
|
+
signing_algorithm: oauth_application[oauth_applications_id_token_signed_response_alg_column] || oauth_jwt_algorithm,
|
358
|
+
encryption_algorithm: oauth_application[oauth_applications_id_token_encrypted_response_alg_column],
|
359
|
+
encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
|
360
|
+
}
|
361
|
+
oauth_token[:id_token] = jwt_encode(id_token_claims, **params)
|
334
362
|
end
|
335
363
|
|
336
364
|
# aka fill_with_standard_claims
|
@@ -443,7 +471,7 @@ module Rodauth
|
|
443
471
|
|
444
472
|
# Metadata
|
445
473
|
|
446
|
-
def openid_configuration_body(path)
|
474
|
+
def openid_configuration_body(path = nil)
|
447
475
|
metadata = oauth_server_metadata_body(path).select do |k, _|
|
448
476
|
VALID_METADATA_KEYS.include?(k)
|
449
477
|
end
|
@@ -461,7 +489,8 @@ module Rodauth
|
|
461
489
|
scope_claims.unshift("auth_time") if last_account_login_at
|
462
490
|
|
463
491
|
response_types_supported = metadata[:response_types_supported]
|
464
|
-
|
492
|
+
|
493
|
+
if metadata[:grant_types_supported].include?("implicit")
|
465
494
|
response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
|
466
495
|
end
|
467
496
|
|
@@ -503,7 +532,7 @@ module Rodauth
|
|
503
532
|
response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
504
533
|
response["Access-Control-Max-Age"] = "3600"
|
505
534
|
response.status = 200
|
506
|
-
|
535
|
+
return_response
|
507
536
|
end
|
508
537
|
end
|
509
538
|
end
|