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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -424
  3. data/README.md +26 -389
  4. data/doc/release_notes/0_0_1.md +3 -0
  5. data/doc/release_notes/0_0_2.md +15 -0
  6. data/doc/release_notes/0_0_3.md +31 -0
  7. data/doc/release_notes/0_0_4.md +36 -0
  8. data/doc/release_notes/0_0_5.md +36 -0
  9. data/doc/release_notes/0_0_6.md +21 -0
  10. data/doc/release_notes/0_1_0.md +44 -0
  11. data/doc/release_notes/0_2_0.md +43 -0
  12. data/doc/release_notes/0_3_0.md +28 -0
  13. data/doc/release_notes/0_4_0.md +18 -0
  14. data/doc/release_notes/0_4_1.md +9 -0
  15. data/doc/release_notes/0_4_2.md +5 -0
  16. data/doc/release_notes/0_4_3.md +3 -0
  17. data/doc/release_notes/0_5_0.md +11 -0
  18. data/doc/release_notes/0_5_1.md +13 -0
  19. data/doc/release_notes/0_6_0.md +9 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_7_0.md +20 -0
  22. data/doc/release_notes/0_7_1.md +10 -0
  23. data/doc/release_notes/0_7_2.md +21 -0
  24. data/doc/release_notes/0_7_3.md +10 -0
  25. data/doc/release_notes/0_7_4.md +5 -0
  26. data/doc/release_notes/0_8_0.md +37 -0
  27. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
  28. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
  29. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
  30. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
  31. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
  32. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
  33. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
  34. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
  35. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
  36. data/lib/rodauth/features/oauth.rb +3 -1418
  37. data/lib/rodauth/features/oauth_application_management.rb +209 -0
  38. data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
  39. data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
  40. data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
  41. data/lib/rodauth/features/oauth_base.rb +735 -0
  42. data/lib/rodauth/features/oauth_device_grant.rb +221 -0
  43. data/lib/rodauth/features/oauth_http_mac.rb +3 -21
  44. data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
  45. data/lib/rodauth/features/oauth_jwt.rb +37 -60
  46. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
  47. data/lib/rodauth/features/oauth_pkce.rb +98 -0
  48. data/lib/rodauth/features/oauth_resource_server.rb +21 -0
  49. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
  50. data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
  51. data/lib/rodauth/features/oauth_token_management.rb +77 -0
  52. data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
  53. data/lib/rodauth/features/oidc.rb +4 -3
  54. data/lib/rodauth/oauth/database_extensions.rb +15 -2
  55. data/lib/rodauth/oauth/refinements.rb +48 -0
  56. data/lib/rodauth/oauth/version.rb +1 -1
  57. data/locales/en.yml +28 -12
  58. data/templates/authorize.str +7 -7
  59. data/templates/client_secret_field.str +2 -2
  60. data/templates/description_field.str +1 -1
  61. data/templates/device_search.str +11 -0
  62. data/templates/device_verification.str +24 -0
  63. data/templates/homepage_url_field.str +2 -2
  64. data/templates/jws_jwk_field.str +4 -0
  65. data/templates/jwt_public_key_field.str +4 -0
  66. data/templates/name_field.str +1 -1
  67. data/templates/new_oauth_application.str +9 -0
  68. data/templates/oauth_application.str +7 -3
  69. data/templates/oauth_application_oauth_tokens.str +51 -0
  70. data/templates/oauth_applications.str +2 -2
  71. data/templates/oauth_tokens.str +9 -11
  72. data/templates/redirect_uri_field.str +2 -2
  73. metadata +71 -3
  74. 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
- # obly works if select_account plugin is available
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
- if use_oauth_implicit_grant_type?
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, Metrics/ParameterLists
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, Metrics/ParameterLists
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.7.4"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  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
- oauth_oauth_applications_page_title: "Oauth Applications"
10
- oauth_oauth_application_page_title: "Oauth Application"
11
- oauth_new_oauth_application_page_title: "New Oauth Application"
12
- oauth_oauth_tokens_page_title: "Oauth Tokens"
13
- name_label: "Name"
14
- description_label: "Description"
15
- scopes_label: "Scopes"
16
- homepage_url_label: "Homepage URL"
17
- redirect_uri_label: "Redirect URL"
18
- client_secret_label: "Client Secret"
19
- client_id_label: "Client ID"
20
- oauth_applications_button: "Register"
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"
@@ -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.scopes_label}</h1>
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">Cancel</a>
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.client_secret_label}#{rodauth.input_field_label_suffix}</label>
3
- #{rodauth.input_field_string(rodauth.oauth_application_client_secret_param, "client_secret", :type=>"text")}
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.description_label}#{rodauth.input_field_label_suffix}</label>
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>