graphql_devise 0.13.3 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -0
  3. data/Appraisals +14 -0
  4. data/CHANGELOG.md +47 -0
  5. data/README.md +35 -22
  6. data/app/models/graphql_devise/concerns/model.rb +6 -6
  7. data/app/views/graphql_devise/mailer/reset_password_instructions.html.erb +7 -1
  8. data/config/locales/en.yml +2 -1
  9. data/docs/usage/reset_password_flow.md +90 -0
  10. data/graphql_devise.gemspec +1 -1
  11. data/lib/graphql_devise/concerns/controller_methods.rb +7 -1
  12. data/lib/graphql_devise/default_operations/mutations.rb +10 -6
  13. data/lib/graphql_devise/mutations/resend_confirmation.rb +15 -5
  14. data/lib/graphql_devise/mutations/send_password_reset.rb +2 -0
  15. data/lib/graphql_devise/mutations/send_password_reset_with_token.rb +37 -0
  16. data/lib/graphql_devise/mutations/sign_up.rb +1 -3
  17. data/lib/graphql_devise/mutations/update_password_with_token.rb +38 -0
  18. data/lib/graphql_devise/resolvers/check_password_token.rb +1 -0
  19. data/lib/graphql_devise/resolvers/confirm_account.rb +2 -0
  20. data/lib/graphql_devise/schema_plugin.rb +12 -10
  21. data/lib/graphql_devise/version.rb +1 -1
  22. data/spec/dummy/app/graphql/mutations/reset_admin_password_with_token.rb +13 -0
  23. data/spec/dummy/config/initializers/devise_token_auth.rb +2 -0
  24. data/spec/dummy/config/routes.rb +2 -1
  25. data/spec/graphql/user_queries_spec.rb +118 -0
  26. data/spec/requests/mutations/additional_mutations_spec.rb +0 -1
  27. data/spec/requests/mutations/resend_confirmation_spec.rb +42 -4
  28. data/spec/requests/mutations/send_password_reset_spec.rb +16 -1
  29. data/spec/requests/mutations/send_password_reset_with_token_spec.rb +78 -0
  30. data/spec/requests/mutations/sign_up_spec.rb +19 -1
  31. data/spec/requests/mutations/update_password_with_token_spec.rb +119 -0
  32. data/spec/requests/queries/check_password_token_spec.rb +16 -1
  33. data/spec/requests/queries/confirm_account_spec.rb +17 -2
  34. data/spec/requests/user_controller_spec.rb +9 -9
  35. data/spec/support/contexts/schema_test.rb +14 -0
  36. metadata +21 -8
@@ -9,15 +9,16 @@ module GraphqlDevise
9
9
  field :message, String, null: false
10
10
 
11
11
  def resolve(email:, redirect_url:)
12
- resource = find_resource(
13
- :email,
14
- get_case_insensitive_field(:email, email)
15
- )
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
14
+ resource = find_confirmable_resource(email)
16
15
 
17
16
  if resource
18
17
  yield resource if block_given?
19
18
 
20
- raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed')) if resource.confirmed?
19
+ if resource.confirmed? && !resource.pending_reconfirmation?
20
+ raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
21
+ end
21
22
 
22
23
  resource.send_confirmation_instructions(
23
24
  redirect_url: redirect_url,
@@ -30,6 +31,15 @@ module GraphqlDevise
30
31
  raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
31
32
  end
32
33
  end
34
+
35
+ private
36
+
37
+ def find_confirmable_resource(email)
38
+ email_insensitive = get_case_insensitive_field(:email, email)
39
+ resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
40
+ resource ||= find_resource(:email, email_insensitive)
41
+ resource
42
+ end
33
43
  end
34
44
  end
35
45
  end
@@ -9,6 +9,8 @@ module GraphqlDevise
9
9
  field :message, String, null: false
10
10
 
11
11
  def resolve(email:, redirect_url:)
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
12
14
  resource = find_resource(:email, get_case_insensitive_field(:email, email))
13
15
 
14
16
  if resource
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class SendPasswordResetWithToken < Base
6
+ argument :email, String, required: true
7
+ argument :redirect_url, String, required: true
8
+
9
+ field :message, String, null: false
10
+
11
+ def resolve(email:, redirect_url:)
12
+ check_redirect_url_whitelist!(redirect_url)
13
+
14
+ resource = find_resource(:email, get_case_insensitive_field(:email, email))
15
+
16
+ if resource
17
+ yield resource if block_given?
18
+
19
+ resource.send_reset_password_instructions(
20
+ email: email,
21
+ provider: 'email',
22
+ redirect_url: redirect_url,
23
+ template_path: ['graphql_devise/mailer']
24
+ )
25
+
26
+ if resource.errors.empty?
27
+ { message: I18n.t('graphql_devise.passwords.send_instructions') }
28
+ else
29
+ raise_user_error_list(I18n.t('graphql_devise.invalid_resource'), errors: resource.errors.full_messages)
30
+ end
31
+ else
32
+ raise_user_error(I18n.t('graphql_devise.user_not_found'))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -22,9 +22,7 @@ module GraphqlDevise
22
22
  raise_user_error(I18n.t('graphql_devise.registrations.missing_confirm_redirect_url'))
23
23
  end
24
24
 
25
- if blacklisted_redirect_url?(redirect_url)
26
- raise_user_error(I18n.t('graphql_devise.registrations.redirect_url_not_allowed', redirect_url: redirect_url))
27
- end
25
+ check_redirect_url_whitelist!(redirect_url)
28
26
 
29
27
  resource.skip_confirmation_notification! if resource.respond_to?(:skip_confirmation_notification!)
30
28
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlDevise
4
+ module Mutations
5
+ class UpdatePasswordWithToken < Base
6
+ argument :password, String, required: true
7
+ argument :password_confirmation, String, required: true
8
+ argument :reset_password_token, String, required: true
9
+
10
+ field :credentials,
11
+ GraphqlDevise::Types::CredentialType,
12
+ null: true,
13
+ description: 'Authentication credentials. Resource must be signed_in for credentials to be returned.'
14
+
15
+ def resolve(reset_password_token:, **attrs)
16
+ raise_user_error(I18n.t('graphql_devise.passwords.password_recovery_disabled')) unless recoverable_enabled?
17
+
18
+ resource = resource_class.with_reset_password_token(reset_password_token)
19
+ raise_user_error(I18n.t('graphql_devise.passwords.reset_token_not_found')) if resource.blank?
20
+ raise_user_error(I18n.t('graphql_devise.passwords.reset_token_expired')) unless resource.reset_password_period_valid?
21
+
22
+ if resource.update(attrs)
23
+ yield resource if block_given?
24
+
25
+ response_payload = { authenticatable: resource }
26
+ response_payload[:credentials] = set_auth_headers(resource) if controller.signed_in?(resource_name)
27
+
28
+ response_payload
29
+ else
30
+ raise_user_error_list(
31
+ I18n.t('graphql_devise.passwords.update_password_error'),
32
+ errors: resource.errors.full_messages
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -27,6 +27,7 @@ module GraphqlDevise
27
27
  )
28
28
 
29
29
  if redirect_url.present?
30
+ check_redirect_url_whitelist!(redirect_url)
30
31
  controller.redirect_to(resource.build_auth_url(redirect_url, built_redirect_headers))
31
32
  else
32
33
  set_auth_headers(resource)
@@ -7,6 +7,8 @@ module GraphqlDevise
7
7
  argument :redirect_url, String, required: true
8
8
 
9
9
  def resolve(confirmation_token:, redirect_url:)
10
+ check_redirect_url_whitelist!(redirect_url)
11
+
10
12
  resource = resource_class.confirm_by_token(confirmation_token)
11
13
 
12
14
  if resource.errors.empty?
@@ -24,13 +24,12 @@ module GraphqlDevise
24
24
  # Authenticate only root level queries
25
25
  return yield unless event == 'execute_field' && path(trace_data).count == 1
26
26
 
27
- field = traced_field(trace_data)
28
- provided_value = authenticate_option(field, trace_data)
29
- context = set_current_resource(context_from_data(trace_data))
27
+ field = traced_field(trace_data)
28
+ auth_required = authenticate_option(field, trace_data)
29
+ context = context_from_data(trace_data)
30
30
 
31
- if !provided_value.nil?
32
- raise_on_missing_resource(context, field) if provided_value
33
- elsif @authenticate_default
31
+ if auth_required
32
+ context = set_current_resource(context)
34
33
  raise_on_missing_resource(context, field)
35
34
  end
36
35
 
@@ -40,9 +39,10 @@ module GraphqlDevise
40
39
  private
41
40
 
42
41
  def set_current_resource(context)
43
- controller = context[:controller]
44
- resource_names = Array(context[:resource_name])
45
- context[:current_resource] = resource_names.find do |resource_name|
42
+ controller = context[:controller]
43
+ resource_names = Array(context[:resource_name])
44
+
45
+ context[:current_resource] ||= resource_names.find do |resource_name|
46
46
  unless Devise.mappings.key?(resource_name)
47
47
  raise(
48
48
  GraphqlDevise::Error,
@@ -88,11 +88,13 @@ module GraphqlDevise
88
88
  end
89
89
 
90
90
  def authenticate_option(field, trace_data)
91
- if trace_data[:context]
91
+ auth_required = if trace_data[:context]
92
92
  field.metadata[:authenticate]
93
93
  else
94
94
  field.graphql_definition.metadata[:authenticate]
95
95
  end
96
+
97
+ auth_required.nil? ? @authenticate_default : auth_required
96
98
  end
97
99
 
98
100
  def reconfigure_warden!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlDevise
4
- VERSION = '0.13.3'.freeze
4
+ VERSION = '0.14.1'.freeze
5
5
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutations
4
+ class ResetAdminPasswordWithToken < GraphqlDevise::Mutations::UpdatePasswordWithToken
5
+ field :authenticatable, Types::AdminType, null: false
6
+
7
+ def resolve(reset_password_token:, **attrs)
8
+ super do |admin|
9
+ controller.sign_in(admin)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -39,6 +39,8 @@ DeviseTokenAuth.setup do |config|
39
39
 
40
40
  config.default_confirm_success_url = 'https://google.com'
41
41
 
42
+ config.redirect_whitelist = ['https://google.com']
43
+
42
44
  # By default we will use callbacks for single omniauth.
43
45
  # It depends on fields like email, provider and uid.
44
46
  # config.default_callbacks = true
@@ -15,7 +15,8 @@ Rails.application.routes.draw do
15
15
  authenticatable_type: Types::CustomAdminType,
16
16
  skip: [:sign_up, :check_password_token],
17
17
  operations: {
18
- confirm_account: Resolvers::ConfirmAdminAccount
18
+ confirm_account: Resolvers::ConfirmAdminAccount,
19
+ update_password_with_token: Mutations::ResetAdminPasswordWithToken
19
20
  },
20
21
  at: '/api/v1/admin/graphql_auth'
21
22
  )
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe 'Users controller specs' do
6
+ include_context 'with graphql schema test'
7
+
8
+ let(:schema) { DummySchema }
9
+ let(:user) { create(:user, :confirmed) }
10
+ let(:field) { 'privateField' }
11
+ let(:public_message) { 'Field does not require authentication' }
12
+ let(:private_message) { 'Field will always require authentication' }
13
+ let(:private_error) do
14
+ {
15
+ message: "#{field} field requires authentication",
16
+ extensions: { code: 'AUTHENTICATION_ERROR' }
17
+ }
18
+ end
19
+
20
+ describe 'publicField' do
21
+ let(:query) do
22
+ <<-GRAPHQL
23
+ query {
24
+ publicField
25
+ }
26
+ GRAPHQL
27
+ end
28
+
29
+ context 'when using a regular schema' do
30
+ it 'does not require authentication' do
31
+ expect(response[:data][:publicField]).to eq(public_message)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe 'privateField' do
37
+ let(:query) do
38
+ <<-GRAPHQL
39
+ query {
40
+ privateField
41
+ }
42
+ GRAPHQL
43
+ end
44
+
45
+ context 'when using a regular schema' do
46
+ context 'when user is authenticated' do
47
+ let(:resource) { user }
48
+
49
+ it 'allows to perform the query' do
50
+ expect(response[:data][:privateField]).to eq(private_message)
51
+ end
52
+
53
+ context 'when using a SchemaUser' do
54
+ let(:resource) { create(:schema_user, :confirmed) }
55
+
56
+ it 'allows to perform the query' do
57
+ expect(response[:data][:privateField]).to eq(private_message)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'when using an interpreter schema' do
64
+ let(:schema) { InterpreterSchema }
65
+
66
+ context 'when user is authenticated' do
67
+ let(:resource) { user }
68
+
69
+ it 'allows to perform the query' do
70
+ expect(response[:data][:privateField]).to eq(private_message)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ describe 'user' do
77
+ let(:user_data) { { email: user.email, id: user.id } }
78
+ let(:query) do
79
+ <<-GRAPHQL
80
+ query {
81
+ user(id: #{user.id}) {
82
+ id
83
+ email
84
+ }
85
+ }
86
+ GRAPHQL
87
+ end
88
+
89
+ context 'when using a regular schema' do
90
+ context 'when user is authenticated' do
91
+ let(:resource) { user }
92
+
93
+ it 'allows to perform the query' do
94
+ expect(response[:data][:user]).to match(**user_data)
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'when using an interpreter schema' do
100
+ let(:schema) { InterpreterSchema }
101
+
102
+ context 'when user is authenticated' do
103
+ let(:resource) { user }
104
+
105
+ it 'allows to perform the query' do
106
+ expect(response[:data][:user]).to match(**user_data)
107
+ end
108
+ end
109
+
110
+ context 'when user is not authenticated' do
111
+ # Interpreter schema fields are public unless specified otherwise (plugin setting)
112
+ it 'allows to perform the query' do
113
+ expect(response[:data][:user]).to match(**user_data)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -9,7 +9,6 @@ RSpec.describe 'Additional Mutations' do
9
9
  let(:password) { Faker::Internet.password }
10
10
  let(:password_confirmation) { password }
11
11
  let(:email) { Faker::Internet.email }
12
- let(:redirect) { Faker::Internet.url }
13
12
 
14
13
  context 'when using the user model' do
15
14
  let(:query) do
@@ -5,10 +5,11 @@ require 'rails_helper'
5
5
  RSpec.describe 'Resend confirmation' do
6
6
  include_context 'with graphql query request'
7
7
 
8
- let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
9
- let(:email) { user.email }
10
- let(:id) { user.id }
11
- let(:redirect) { Faker::Internet.url }
8
+ let(:confirmed_at) { nil }
9
+ let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
10
+ let(:email) { user.email }
11
+ let(:id) { user.id }
12
+ let(:redirect) { 'https://google.com' }
12
13
  let(:query) do
13
14
  <<-GRAPHQL
14
15
  mutation {
@@ -22,6 +23,21 @@ RSpec.describe 'Resend confirmation' do
22
23
  GRAPHQL
23
24
  end
24
25
 
26
+ context 'when redirect_url is not whitelisted' do
27
+ let(:redirect) { 'https://not-safe.com' }
28
+
29
+ it 'returns a not whitelisted redirect url error' do
30
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
31
+
32
+ expect(json_response[:errors]).to containing_exactly(
33
+ hash_including(
34
+ message: "Redirect to '#{redirect}' not allowed.",
35
+ extensions: { code: 'USER_ERROR' }
36
+ )
37
+ )
38
+ end
39
+ end
40
+
25
41
  context 'when params are correct' do
26
42
  context 'when using the gem schema' do
27
43
  it 'sends an email to the user with confirmation url and returns a success message' do
@@ -98,6 +114,28 @@ RSpec.describe 'Resend confirmation' do
98
114
  end
99
115
  end
100
116
 
117
+ context 'when the email was changed' do
118
+ let(:confirmed_at) { 2.seconds.ago }
119
+ let(:email) { 'new-email@wallaceinc.com' }
120
+ let(:new_email) { email }
121
+
122
+ before do
123
+ user.update_with_email(
124
+ email: new_email,
125
+ schema_url: 'http://localhost/test',
126
+ confirmation_success_url: 'https://google.com'
127
+ )
128
+ end
129
+
130
+ it 'sends new confirmation email' do
131
+ expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
132
+ expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(new_email)
133
+ expect(json_response[:data][:userResendConfirmation]).to include(
134
+ message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
135
+ )
136
+ end
137
+ end
138
+
101
139
  context "when the email isn't in the system" do
102
140
  let(:email) { 'nothere@gmail.com' }
103
141
 
@@ -7,7 +7,7 @@ RSpec.describe 'Send Password Reset Requests' do
7
7
 
8
8
  let!(:user) { create(:user, :confirmed, email: 'jwinnfield@wallaceinc.com') }
9
9
  let(:email) { user.email }
10
- let(:redirect_url) { Faker::Internet.url }
10
+ let(:redirect_url) { 'https://google.com' }
11
11
  let(:query) do
12
12
  <<-GRAPHQL
13
13
  mutation {
@@ -21,6 +21,21 @@ RSpec.describe 'Send Password Reset Requests' do
21
21
  GRAPHQL
22
22
  end
23
23
 
24
+ context 'when redirect_url is not whitelisted' do
25
+ let(:redirect_url) { 'https://not-safe.com' }
26
+
27
+ it 'returns a not whitelisted redirect url error' do
28
+ expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
29
+
30
+ expect(json_response[:errors]).to containing_exactly(
31
+ hash_including(
32
+ message: "Redirect to '#{redirect_url}' not allowed.",
33
+ extensions: { code: 'USER_ERROR' }
34
+ )
35
+ )
36
+ end
37
+ end
38
+
24
39
  context 'when params are correct' do
25
40
  context 'when using the gem schema' do
26
41
  it 'sends password reset email' do