rodauth-oauth 0.7.4 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|