rodauth-oauth 0.7.4 → 0.8.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 -424
- data/README.md +26 -389
- 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/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
- 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 +22 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +209 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +735 -0
- data/lib/rodauth/features/oauth_device_grant.rb +221 -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 +37 -60
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -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 +77 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +4 -3
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +28 -12
- data/templates/authorize.str +7 -7
- 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/jws_jwk_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 +51 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_tokens.str +9 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +71 -3
- data/lib/rodauth/features/oauth_saml.rb +0 -104
@@ -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,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:oauth_token_management, :OauthTokenManagement) do
|
5
|
+
using RegexpExtensions
|
6
|
+
|
7
|
+
depends :oauth_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
|
+
|
22
|
+
auth_value_methods(
|
23
|
+
:oauth_token_path
|
24
|
+
)
|
25
|
+
|
26
|
+
def oauth_tokens_path(opts = {})
|
27
|
+
route_path(oauth_tokens_route, opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def oauth_tokens_url(opts = {})
|
31
|
+
route_url(oauth_tokens_route, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
def oauth_token_path(id)
|
35
|
+
"#{oauth_tokens_path}/#{id}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def oauth_tokens
|
39
|
+
request.on(oauth_tokens_route) do
|
40
|
+
require_account
|
41
|
+
|
42
|
+
request.get do
|
43
|
+
scope.instance_variable_set(:@oauth_tokens, db[oauth_tokens_table]
|
44
|
+
.select(Sequel[oauth_tokens_table].*, Sequel[oauth_applications_table][oauth_applications_name_column])
|
45
|
+
.join(oauth_applications_table, Sequel[oauth_tokens_table][oauth_tokens_oauth_application_id_column] =>
|
46
|
+
Sequel[oauth_applications_table][oauth_applications_id_column])
|
47
|
+
.where(Sequel[oauth_tokens_table][oauth_tokens_account_id_column] => account_id)
|
48
|
+
.where(oauth_tokens_revoked_at_column => nil))
|
49
|
+
oauth_tokens_view
|
50
|
+
end
|
51
|
+
|
52
|
+
request.post(oauth_tokens_id_pattern) do |id|
|
53
|
+
db[oauth_tokens_table]
|
54
|
+
.where(oauth_tokens_id_column => id)
|
55
|
+
.where(oauth_tokens_account_id_column => account_id)
|
56
|
+
.update(oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
|
57
|
+
|
58
|
+
set_notice_flash revoke_oauth_token_notice_flash
|
59
|
+
redirect oauth_tokens_path || "/"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_csrf?
|
65
|
+
case request.path
|
66
|
+
when oauth_tokens_path
|
67
|
+
only_json? ? false : super
|
68
|
+
else
|
69
|
+
super
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_valid_uri?(uri)
|
74
|
+
URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
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
|
@@ -283,7 +283,7 @@ module Rodauth
|
|
283
283
|
redirect_response_error("consent_required")
|
284
284
|
end
|
285
285
|
when "select-account"
|
286
|
-
#
|
286
|
+
# only works if select_account plugin is available
|
287
287
|
require_select_account if respond_to?(:require_select_account)
|
288
288
|
else
|
289
289
|
redirect_response_error("invalid_request")
|
@@ -302,7 +302,7 @@ module Rodauth
|
|
302
302
|
super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
|
303
303
|
end
|
304
304
|
|
305
|
-
def create_oauth_token
|
305
|
+
def create_oauth_token(*)
|
306
306
|
oauth_token = super
|
307
307
|
generate_id_token(oauth_token)
|
308
308
|
oauth_token
|
@@ -461,7 +461,8 @@ module Rodauth
|
|
461
461
|
scope_claims.unshift("auth_time") if last_account_login_at
|
462
462
|
|
463
463
|
response_types_supported = metadata[:response_types_supported]
|
464
|
-
|
464
|
+
|
465
|
+
if metadata[:grant_types_supported].include?("implicit")
|
465
466
|
response_types_supported += ["none", "id_token", "code token", "code id_token", "id_token token", "code id_token token"]
|
466
467
|
end
|
467
468
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Rodauth
|
4
4
|
module OAuth
|
5
|
-
# rubocop:disable Naming/MethodName
|
5
|
+
# rubocop:disable Naming/MethodName
|
6
6
|
def self.ExtendDatabase(db)
|
7
7
|
Module.new do
|
8
8
|
dataset = db.dataset
|
@@ -42,6 +42,14 @@ module Rodauth
|
|
42
42
|
|
43
43
|
__insert_and_return__(dataset, pkey, params)
|
44
44
|
end
|
45
|
+
|
46
|
+
def __insert_or_do_nothing_and_return__(dataset, pkey, unique_columns, params)
|
47
|
+
__insert_and_return__(
|
48
|
+
dataset.insert_conflict(target: unique_columns),
|
49
|
+
pkey,
|
50
|
+
params
|
51
|
+
) || dataset.where(params).first
|
52
|
+
end
|
45
53
|
else
|
46
54
|
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
|
47
55
|
find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
|
@@ -65,9 +73,14 @@ module Rodauth
|
|
65
73
|
__insert_and_return__(dataset, pkey, params)
|
66
74
|
end
|
67
75
|
end
|
76
|
+
|
77
|
+
def __insert_or_do_nothing_and_return__(dataset, pkey, unique_columns, params)
|
78
|
+
find_params = params.select { |key, _| unique_columns.include?(key) }
|
79
|
+
dataset.where(find_params).first || __insert_and_return__(dataset, pkey, params)
|
80
|
+
end
|
68
81
|
end
|
69
82
|
end
|
70
83
|
end
|
71
|
-
# rubocop:enable Naming/MethodName
|
84
|
+
# rubocop:enable Naming/MethodName
|
72
85
|
end
|
73
86
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
module PrefixExtensions
|
5
|
+
unless String.method_defined?(:delete_prefix)
|
6
|
+
refine(String) do
|
7
|
+
def delete_suffix(suffix)
|
8
|
+
suffix = suffix.to_s
|
9
|
+
len = suffix.length
|
10
|
+
return dup unless len.positive? && index(suffix, -len)
|
11
|
+
|
12
|
+
self[0...-len]
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete_prefix(prefix)
|
16
|
+
prefix = prefix.to_s
|
17
|
+
return dup unless rindex(prefix, 0)
|
18
|
+
|
19
|
+
self[prefix.length..-1]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
unless String.method_defined?(:delete_suffix!)
|
25
|
+
refine(String) do
|
26
|
+
def delete_suffix!(suffix)
|
27
|
+
suffix = suffix.to_s
|
28
|
+
chomp! if frozen?
|
29
|
+
len = suffix.length
|
30
|
+
return unless len.positive? && index(suffix, -len)
|
31
|
+
|
32
|
+
self[-len..-1] = ""
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module RegexpExtensions
|
40
|
+
unless Regexp.method_defined?(:match?)
|
41
|
+
refine(Regexp) do
|
42
|
+
def match?(*args)
|
43
|
+
!match(*args).nil?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/locales/en.yml
CHANGED
@@ -5,22 +5,34 @@ en:
|
|
5
5
|
create_oauth_application_notice_flash: "Your oauth application has been registered"
|
6
6
|
revoke_unauthorized_account_error_flash: "You are not authorized to revoke this token"
|
7
7
|
revoke_oauth_token_notice_flash: "The oauth token has been revoked"
|
8
|
+
device_verification_notice_flash: "The device is verified"
|
9
|
+
user_code_not_found_error_flash: "No device to authorize with the given user code"
|
8
10
|
oauth_authorize_title: "Authorize"
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
oauth_applications_page_title: "Oauth Applications"
|
12
|
+
oauth_application_page_title: "Oauth Application"
|
13
|
+
new_oauth_application_page_title: "New Oauth Application"
|
14
|
+
oauth_application_oauth_tokens_page_title: "Application Oauth Tokens"
|
15
|
+
oauth_tokens_page_title: "My Oauth Tokens"
|
16
|
+
device_verification_page_title: "Device Verification"
|
17
|
+
device_search_page_title: "Device Search"
|
18
|
+
oauth_applications_name_label: "Name"
|
19
|
+
oauth_applications_description_label: "Description"
|
20
|
+
oauth_applications_scopes_label: "Scopes"
|
21
|
+
oauth_applications_homepage_url_label: "Homepage URL"
|
22
|
+
oauth_applications_redirect_uri_label: "Redirect URL"
|
23
|
+
oauth_applications_client_secret_label: "Client Secret"
|
24
|
+
oauth_applications_client_id_label: "Client ID"
|
25
|
+
oauth_grant_user_code_label: "User code"
|
26
|
+
oauth_grant_user_jws_jwk_label: "JSON Web Keys"
|
27
|
+
oauth_grant_user_jwt_public_key_label: "Public key"
|
28
|
+
oauth_application_button: "Register"
|
21
29
|
oauth_authorize_button: "Authorize"
|
22
30
|
oauth_token_revoke_button: "Revoke"
|
23
31
|
oauth_authorize_post_button: "Back to Client Application"
|
32
|
+
oauth_device_verification_button: "Verify"
|
33
|
+
oauth_device_search_button: "Search"
|
34
|
+
invalid_client_message: "Client authentication failed"
|
35
|
+
invalid_grant_type_message: "Invalid grant type"
|
24
36
|
invalid_grant_message: "Invalid grant"
|
25
37
|
invalid_scope_message: "Invalid scope"
|
26
38
|
invalid_url_message: "Invalid URL"
|
@@ -28,6 +40,10 @@ en:
|
|
28
40
|
unique_error_message: "is already in use"
|
29
41
|
null_error_message: "is not filled"
|
30
42
|
already_in_use_message: "error generating unique token"
|
43
|
+
expired_token_message: "the device code has expired"
|
44
|
+
access_denied_message: "the authorization request has been denied"
|
45
|
+
authorization_pending_message: "the authorization request is still pending"
|
46
|
+
slow_down_message: "authorization request is still pending but poll interval should be increased"
|
31
47
|
code_challenge_required_message: "code challenge required"
|
32
48
|
unsupported_transform_algorithm_message: "transform algorithm not supported"
|
33
49
|
request_uri_not_supported_message: "request uri is unsupported"
|
data/templates/authorize.str
CHANGED
@@ -3,23 +3,23 @@
|
|
3
3
|
<p class="lead">The application #{rodauth.oauth_application[rodauth.oauth_applications_name_column]} would like to access your data.</p>
|
4
4
|
|
5
5
|
<div class="form-group">
|
6
|
-
<h1 class="display-6">#{rodauth.
|
6
|
+
<h1 class="display-6">#{rodauth.oauth_tokens_scopes_label}</h1>
|
7
7
|
|
8
8
|
#{
|
9
9
|
rodauth.scopes.map do |scope|
|
10
10
|
if scope == rodauth.oauth_application_default_scope
|
11
11
|
<<-HTML
|
12
12
|
<div class="form-check">
|
13
|
-
<input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{scope}" checked disabled>
|
14
|
-
<label class="form-check-label" for="#{scope}">#{scope}</label>
|
15
|
-
<input type="hidden" name="scope[]" value="#{scope}">
|
13
|
+
<input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{h(scope)}" checked disabled>
|
14
|
+
<label class="form-check-label" for="#{scope}">#{h(scope)}</label>
|
15
|
+
<input type="hidden" name="scope[]" value="#{h(scope)}">
|
16
16
|
</div>
|
17
17
|
HTML
|
18
18
|
else
|
19
19
|
<<-HTML
|
20
20
|
<div class="form-check">
|
21
|
-
<input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{scope}">
|
22
|
-
<label class="form-check-label" for="#{scope}">#{scope}</label>
|
21
|
+
<input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{h(scope)}">
|
22
|
+
<label class="form-check-label" for="#{scope}">#{h(scope)}</label>
|
23
23
|
</div>
|
24
24
|
HTML
|
25
25
|
end
|
@@ -39,6 +39,6 @@
|
|
39
39
|
</div>
|
40
40
|
<p class="text-center">
|
41
41
|
<input type="submit" class="btn btn-outline-primary" value="#{h(rodauth.oauth_authorize_button)}"/>
|
42
|
-
<a href="#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{ "&state=#{rodauth.param("state")}" if rodauth.param_or_nil("state")}" class="btn btn-outline-danger"
|
42
|
+
<a href="#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{ "&state=#{rodauth.param("state")}" if rodauth.param_or_nil("state")}" class="btn btn-outline-danger">#{rodauth.oauth_cancel_button}</a>
|
43
43
|
</p>
|
44
44
|
</form>
|
@@ -1,4 +1,4 @@
|
|
1
1
|
<div class="form-group">
|
2
|
-
<label for="client_secret">#{rodauth.
|
3
|
-
#{rodauth.input_field_string(rodauth.oauth_application_client_secret_param, "
|
2
|
+
<label for="client_secret">#{rodauth.oauth_applications_client_secret_label}#{rodauth.input_field_label_suffix}</label>
|
3
|
+
#{rodauth.input_field_string(rodauth.oauth_application_client_secret_param, "client-secret", :type=>"text")}
|
4
4
|
</div>
|
@@ -1,4 +1,4 @@
|
|
1
1
|
<div class="form-group">
|
2
|
-
<label for="description">#{rodauth.
|
2
|
+
<label for="description">#{rodauth.oauth_applications_description_label}#{rodauth.input_field_label_suffix}</label>
|
3
3
|
#{rodauth.input_field_string(rodauth.oauth_application_description_param, "description", :type=>"text", :required => false)}
|
4
4
|
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<form method="get" action="#{rodauth.device_path}" class="form-horizontal" role="form" id="device-search-form">
|
2
|
+
<p class="lead">Insert the user code from the device you'd like to authorize.</p>
|
3
|
+
|
4
|
+
<div class="form-group">
|
5
|
+
<label for="user_code">#{rodauth.oauth_grant_user_code_label}</label>
|
6
|
+
#{rodauth.input_field_string("user_code", "user_code", :value => rodauth.param_or_nil(rodauth.oauth_grant_user_code_param))}
|
7
|
+
</div>
|
8
|
+
<p class="text-center">
|
9
|
+
<input type="submit" class="btn btn-outline-primary" value="#{h(rodauth.oauth_device_search_button)}"/>
|
10
|
+
</p>
|
11
|
+
</form>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<form method="post" action="#{rodauth.device_path}" class="form-horizontal" role="form" id="device-verification-form">
|
2
|
+
#{csrf_tag(rodauth.device_path) if respond_to?(:csrf_tag)}
|
3
|
+
<p class="lead">The device with user code #{@oauth_grant[rodauth.oauth_grants_user_code_column]} would like to access your data.</p>
|
4
|
+
|
5
|
+
<div class="form-group">
|
6
|
+
<h1 class="display-6">#{rodauth.oauth_tokens_scopes_label}</h1>
|
7
|
+
|
8
|
+
<ul class="list-group">
|
9
|
+
#{
|
10
|
+
scopes = @oauth_grant[rodauth.oauth_grants_scopes_column].split(rodauth.oauth_scope_separator)
|
11
|
+
scopes.map do |scope|
|
12
|
+
<<-HTML
|
13
|
+
<li class="list-group-item">#{scope}</li>
|
14
|
+
HTML
|
15
|
+
end.join
|
16
|
+
}
|
17
|
+
</ul>
|
18
|
+
</div>
|
19
|
+
<input type="hidden" name="user_code" value="#{rodauth.param("user_code")}"/>
|
20
|
+
<p class="text-center">
|
21
|
+
<input type="submit" class="btn btn-outline-primary" value="#{h(rodauth.oauth_device_verification_button)}"/>
|
22
|
+
<a href="#{rodauth.device_path}?error=access_denied" class="btn btn-outline-danger">#{rodauth.oauth_cancel_button}</a>
|
23
|
+
</p>
|
24
|
+
</form>
|