graphql_authentication 1.0.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 +7 -0
- data/CHANGELOG.md +58 -0
- data/README.md +132 -0
- data/Rakefile +18 -0
- data/app/graphql/mutations/authentication/forgot_password.rb +29 -0
- data/app/graphql/mutations/authentication/lock_account.rb +31 -0
- data/app/graphql/mutations/authentication/reset_password.rb +48 -0
- data/app/graphql/mutations/authentication/sign_in.rb +58 -0
- data/app/graphql/mutations/authentication/sign_up.rb +44 -0
- data/app/graphql/mutations/authentication/unlock_account.rb +31 -0
- data/app/graphql/mutations/authentication/update_account.rb +51 -0
- data/app/graphql/mutations/authentication/validate_token.rb +30 -0
- data/app/graphql/types/authentication/error.rb +17 -0
- data/app/graphql/types/authentication/user.rb +13 -0
- data/app/graphql/types/graphql_authentication.rb +26 -0
- data/app/helpers/graphql/account_lock_helper.rb +14 -0
- data/app/helpers/graphql/authentication_helper.rb +45 -0
- data/app/helpers/graphql/token_helper.rb +27 -0
- data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
- data/db/migrate/20190108151146_add_refresh_token_to_user.rb +5 -0
- data/db/migrate/20190226175233_add_lockable_to_devise.rb +5 -0
- data/lib/generators/graphql_authentication/install_generator.rb +15 -0
- data/lib/generators/graphql_authentication/templates/graphql_authentication.rb.erb +17 -0
- data/lib/graphql_authentication/configuration.rb +32 -0
- data/lib/graphql_authentication/engine.rb +9 -0
- data/lib/graphql_authentication/jwt_manager.rb +65 -0
- data/lib/graphql_authentication/reset_password.rb +12 -0
- data/lib/graphql_authentication/version.rb +5 -0
- data/lib/graphql_authentication.rb +19 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '0559b2a44d5f3558a14ce497658ccbd7e216435600f215515190017a4e427af0'
|
4
|
+
data.tar.gz: c94bf77fea7d83a7f177f9f0fb4f92ec69b273a5d532ddee6c38eb528f8d9d33
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c22d1ab29125f19c74f3b332088ea80de3db8e03922ed7fa6734ebdecf2327ef46edb81dd45f780761c9f47da47ff4c8b78c3803336be1f3cee42363fee43c9
|
7
|
+
data.tar.gz: ec09ed14e65891f6302efda0aefdd7bd9db1d112cca605f94a5d6da69e8febff828da3ec4691f27948a353c589a952e57067125a96faf376b76cc748c68c9500
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 1.0.0
|
4
|
+
|
5
|
+
### News
|
6
|
+
|
7
|
+
- This gem was forked
|
8
|
+
- Supports Rails 7.x
|
9
|
+
|
10
|
+
## 0.6.1
|
11
|
+
|
12
|
+
Multiple fixes to allow usage of the gem without the lockable Devise
|
13
|
+
feature
|
14
|
+
|
15
|
+
## 0.6.0
|
16
|
+
|
17
|
+
### Important
|
18
|
+
|
19
|
+
Upgrade to 0.6.1 if you plan on using this gem without Devise's lockable
|
20
|
+
feature
|
21
|
+
|
22
|
+
### New features
|
23
|
+
|
24
|
+
Added to possibility to use your own sign_up and update_account mutations
|
25
|
+
to allow custom fields for your user accounts
|
26
|
+
|
27
|
+
### Breaking changes
|
28
|
+
|
29
|
+
Configuration file was changed and some config names now have a different
|
30
|
+
use.
|
31
|
+
|
32
|
+
Please make sure to update your config file with the current version.
|
33
|
+
|
34
|
+
**Those settings were renamed for more clarity**
|
35
|
+
* sign_up_mutation => allow_sign_up
|
36
|
+
* lock_account_mutation => allow_lock_account
|
37
|
+
* unlock_account_mutation => allow_unlock_account
|
38
|
+
|
39
|
+
The updated config file should look like this:
|
40
|
+
```
|
41
|
+
GraphQL::Authentication.configure do |config|
|
42
|
+
# config.token_lifespan = 4.hours
|
43
|
+
# config.jwt_secret_key = ENV['JWT_SECRET_KEY']
|
44
|
+
# config.app_url = ENV['APP_URL']
|
45
|
+
|
46
|
+
# config.user_type = '::Types::Authentication::User'
|
47
|
+
|
48
|
+
# Devise allowed actions
|
49
|
+
# Don't forget to enable the lockable setting in your Devise user model if you plan on using the lock_account feature
|
50
|
+
# config.allow_sign_up = true
|
51
|
+
# config.allow_lock_account = false
|
52
|
+
# config.allow_unlock_account = false
|
53
|
+
|
54
|
+
# Allow custom mutations for signup and update account
|
55
|
+
# config.sign_up_mutation = '::Mutations::Authentication::SignUp'
|
56
|
+
# config.update_account_mutation = '::Mutations::Authentication::UpdateAccount'
|
57
|
+
end
|
58
|
+
```
|
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# GraphQL Authentication
|
2
|
+
|
3
|
+
[](https://travis-ci.org/wbotelhos/graphql_authentication) [](https://codeclimate.com/github/wbotelhos/graphql_authentication/maintainability)
|
4
|
+
[](https://rubygems.org/gems/graphql_authentication)
|
5
|
+
[](https://rubygems.org/gems/graphql_authentication)
|
6
|
+
|
7
|
+
This gem provides an authenticationentication mechanism on a GraphQL API. It use JSON Web Token (JWT) and Devise logic.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'graphql_authentication'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install graphql_authentication
|
24
|
+
|
25
|
+
Then run the installer to create `graphql_authentication.rb` file in your initializers folder.
|
26
|
+
|
27
|
+
```
|
28
|
+
rails g graphql_authentication:install
|
29
|
+
```
|
30
|
+
|
31
|
+
Make sure to read all configurations present inside the file and fill them with your own configs.
|
32
|
+
|
33
|
+
## Devise gem
|
34
|
+
|
35
|
+
Use Devise with a User model and skip all route
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
Rails.application.routes.draw do
|
39
|
+
devise_for :users, skip: :all
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Make 'JWT_SECRET_KEY' and 'APP_URL' available to ENV
|
46
|
+
|
47
|
+
```
|
48
|
+
JWT_SECRET_KEY=
|
49
|
+
APP_URL=
|
50
|
+
```
|
51
|
+
|
52
|
+
Make sure the `Authorization` header is allowed in your api
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
56
|
+
allow do
|
57
|
+
origins '*'
|
58
|
+
resource '*',
|
59
|
+
headers: %w(Authorization Expires RefreshToken),
|
60
|
+
methods: :any,
|
61
|
+
expose: %w(Authorization Expires RefreshToken),
|
62
|
+
max_age: 600
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
Make sure to include `Graphql::AuthenticationHelper` in your `GraphqlController`. A context method returning the current_user will be available
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class GraphqlController < ActionController::API
|
71
|
+
|
72
|
+
include Graphql::AuthenticationHelper
|
73
|
+
|
74
|
+
def execute
|
75
|
+
variables = ensure_hash(params[:variables])
|
76
|
+
query = params[:query]
|
77
|
+
operation_name = params[:operationName]
|
78
|
+
result = ::GraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
|
79
|
+
render json: result
|
80
|
+
|
81
|
+
...
|
82
|
+
```
|
83
|
+
|
84
|
+
Make sure to implement `GraphqlAuthentication` in your `MutationType` to make authentication mutations available
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class Types::MutationType < Types::BaseObject
|
88
|
+
implements ::Types::GraphqlAuthentication
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
## Customization
|
93
|
+
|
94
|
+
If you can to customize any mutation, make sure to update the configurations
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
GraphQL::Authentication.configure do |config|
|
98
|
+
# config.token_lifespan = 4.hours
|
99
|
+
# config.jwt_secret_key = ENV['JWT_SECRET_KEY']
|
100
|
+
# config.app_url = ENV['APP_URL']
|
101
|
+
|
102
|
+
# config.user_type = '::Types::Authentication::User'
|
103
|
+
|
104
|
+
# Devise allowed actions
|
105
|
+
# Don't forget to enable the lockable setting in your Devise user model if you plan on using the lock_account feature
|
106
|
+
# config.allow_sign_up = true
|
107
|
+
# config.allow_lock_account = false
|
108
|
+
# config.allow_unlock_account = false
|
109
|
+
|
110
|
+
# Allow custom mutations for signup and update account
|
111
|
+
# config.sign_up_mutation = '::Mutations::Authentication::SignUp'
|
112
|
+
# config.update_account_mutation = '::Mutations::Authentication::UpdateAccount'
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
## Development
|
117
|
+
|
118
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
119
|
+
|
120
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `graphql_authentication.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
121
|
+
|
122
|
+
## Contributing
|
123
|
+
|
124
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/wbotelhos/graphql_authentication. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
125
|
+
|
126
|
+
## License
|
127
|
+
|
128
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
129
|
+
|
130
|
+
## Code of Conduct
|
131
|
+
|
132
|
+
Everyone interacting in the GraphQL Authentication project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/wbotelhos/graphql_authentication/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
|
8
|
+
load 'rails/tasks/engine.rake'
|
9
|
+
|
10
|
+
require 'rake'
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
|
13
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
14
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
15
|
+
t.rspec_opts = '--format documentation'
|
16
|
+
end
|
17
|
+
|
18
|
+
task default: :spec
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::ForgotPassword < GraphQL::Schema::Mutation
|
4
|
+
include ::Graphql::AccountLockHelper
|
5
|
+
|
6
|
+
argument :email, String, required: true do
|
7
|
+
description 'The email with forgotten password'
|
8
|
+
end
|
9
|
+
|
10
|
+
field :errors, [::Types::Authentication::Error], null: false
|
11
|
+
field :success, Boolean, null: false
|
12
|
+
field :valid, Boolean, null: false
|
13
|
+
|
14
|
+
def resolve(email:)
|
15
|
+
if lockable?
|
16
|
+
user = User.where(locked_at: nil).find_by email: email
|
17
|
+
else
|
18
|
+
user = User.find_by email: email
|
19
|
+
end
|
20
|
+
|
21
|
+
user.send_reset_password_instructions if user.present?
|
22
|
+
|
23
|
+
{
|
24
|
+
errors: [],
|
25
|
+
success: true,
|
26
|
+
valid: true
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::LockAccount < GraphQL::Schema::Mutation
|
4
|
+
argument :id, ID, required: true do
|
5
|
+
description 'User id'
|
6
|
+
end
|
7
|
+
|
8
|
+
field :errors, [::Types::Authentication::Error], null: false
|
9
|
+
field :success, Boolean, null: false
|
10
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
11
|
+
|
12
|
+
def resolve(id:)
|
13
|
+
user = User.where(locked_at: nil).find_by id: id
|
14
|
+
|
15
|
+
if context[:current_user] && user.present? && user.lock_access!
|
16
|
+
{
|
17
|
+
errors: [],
|
18
|
+
success: true,
|
19
|
+
user: user
|
20
|
+
}
|
21
|
+
else
|
22
|
+
{
|
23
|
+
errors: [
|
24
|
+
{ field: :_error, message: I18n.t('devise.locks.cannot_lock') }
|
25
|
+
],
|
26
|
+
success: false,
|
27
|
+
user: user
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::ResetPassword < GraphQL::Schema::Mutation
|
4
|
+
include ::Graphql::AccountLockHelper
|
5
|
+
|
6
|
+
argument :reset_password_token, String, required: true do
|
7
|
+
description "Reset password token"
|
8
|
+
end
|
9
|
+
|
10
|
+
argument :password, String, required: true do
|
11
|
+
description "New user's new password"
|
12
|
+
end
|
13
|
+
|
14
|
+
argument :password_confirmation, String, required: true do
|
15
|
+
description "New user's new password confirmation"
|
16
|
+
end
|
17
|
+
|
18
|
+
field :errors, [::Types::Authentication::Error], null: false
|
19
|
+
field :success, Boolean, null: false
|
20
|
+
|
21
|
+
def resolve(args)
|
22
|
+
if lockable?
|
23
|
+
user = User.where(locked_at: nil).reset_password_by_token args
|
24
|
+
else
|
25
|
+
user = User.reset_password_by_token args
|
26
|
+
end
|
27
|
+
|
28
|
+
if user.errors.any?
|
29
|
+
{
|
30
|
+
success: false,
|
31
|
+
errors: user.errors.messages.map { |field, messages|
|
32
|
+
error_field = field == :reset_password_token ? :_error : field.to_s.camelize(:lower)
|
33
|
+
|
34
|
+
{
|
35
|
+
field: error_field,
|
36
|
+
message: messages.first.capitalize,
|
37
|
+
details: user.errors.details.dig(field)
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
else
|
42
|
+
{
|
43
|
+
errors: [],
|
44
|
+
success: true
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::SignIn < GraphQL::Schema::Mutation
|
4
|
+
include ::Graphql::AccountLockHelper
|
5
|
+
include ::Graphql::TokenHelper
|
6
|
+
|
7
|
+
argument :email, String, required: true do
|
8
|
+
description "The user's email"
|
9
|
+
end
|
10
|
+
|
11
|
+
argument :password, String, required: true do
|
12
|
+
description "The user's password"
|
13
|
+
end
|
14
|
+
|
15
|
+
argument :remember_me, Boolean, required: false do
|
16
|
+
description "User's checkbox to be remembered after connection timeout"
|
17
|
+
end
|
18
|
+
|
19
|
+
field :errors, [::Types::Authentication::Error], null: false
|
20
|
+
field :success, Boolean, null: false
|
21
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
22
|
+
|
23
|
+
def resolve(email:, password:, remember_me:)
|
24
|
+
response = context[:response]
|
25
|
+
|
26
|
+
if lockable?
|
27
|
+
user = User.where(locked_at: nil).find_by email: email
|
28
|
+
else
|
29
|
+
user = User.find_by email: email
|
30
|
+
end
|
31
|
+
|
32
|
+
valid_sign_in = user.present? && user.valid_password?(password)
|
33
|
+
|
34
|
+
if valid_sign_in
|
35
|
+
generate_access_token(user, response)
|
36
|
+
set_current_user(user)
|
37
|
+
remember_me ? set_refresh_token(user, response) : delete_refresh_token(user)
|
38
|
+
|
39
|
+
{
|
40
|
+
errors: [],
|
41
|
+
success: true,
|
42
|
+
user: user
|
43
|
+
}
|
44
|
+
else
|
45
|
+
{
|
46
|
+
errors: [
|
47
|
+
{
|
48
|
+
field: :_error,
|
49
|
+
message: I18n.t('devise.failure.invalid',
|
50
|
+
authenticationentication_keys: I18n.t('activerecord.attributes.user.email'))
|
51
|
+
}
|
52
|
+
],
|
53
|
+
success: false,
|
54
|
+
user: nil
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::SignUp < GraphQL::Schema::Mutation
|
4
|
+
include ::Graphql::TokenHelper
|
5
|
+
|
6
|
+
argument :email, String, required: true do
|
7
|
+
description "New user's email"
|
8
|
+
end
|
9
|
+
|
10
|
+
argument :password, String, required: true do
|
11
|
+
description "New user's password"
|
12
|
+
end
|
13
|
+
|
14
|
+
argument :password_confirmation, String, required: true do
|
15
|
+
description "New user's password confirmation"
|
16
|
+
end
|
17
|
+
|
18
|
+
field :errors, [::Types::Authentication::Error], null: false
|
19
|
+
field :success, Boolean, null: false
|
20
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
21
|
+
|
22
|
+
def resolve(args)
|
23
|
+
response = context[:response]
|
24
|
+
user = User.new args
|
25
|
+
|
26
|
+
if user.save
|
27
|
+
generate_access_token(user, response)
|
28
|
+
|
29
|
+
{
|
30
|
+
errors: [],
|
31
|
+
success: true,
|
32
|
+
user: user
|
33
|
+
}
|
34
|
+
else
|
35
|
+
{
|
36
|
+
errors: user.errors.messages.map do |field, messages|
|
37
|
+
{ field: field.to_s.camelize(:lower), message: messages.first.capitalize }
|
38
|
+
end,
|
39
|
+
success: false,
|
40
|
+
user: nil
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::UnlockAccount < GraphQL::Schema::Mutation
|
4
|
+
argument :id, ID, required: true do
|
5
|
+
description 'User id'
|
6
|
+
end
|
7
|
+
|
8
|
+
field :errors, [::Types::Authentication::Error], null: false
|
9
|
+
field :success, Boolean, null: false
|
10
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
11
|
+
|
12
|
+
def resolve(id:)
|
13
|
+
user = User.where.not(locked_at: nil).find_by id: id
|
14
|
+
|
15
|
+
if context[:current_user] && user.present? && user.unlock_access!
|
16
|
+
{
|
17
|
+
errors: [],
|
18
|
+
success: true,
|
19
|
+
user: user
|
20
|
+
}
|
21
|
+
else
|
22
|
+
{
|
23
|
+
errors: [
|
24
|
+
{ field: :_error, message: I18n.t('devise.unlocks.cannot_unlock') }
|
25
|
+
],
|
26
|
+
success: false,
|
27
|
+
user: user
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::UpdateAccount < GraphQL::Schema::Mutation
|
4
|
+
argument :current_password, String, required: true do
|
5
|
+
description "User's current password"
|
6
|
+
end
|
7
|
+
|
8
|
+
argument :password, String, required: true do
|
9
|
+
description "User's new password"
|
10
|
+
end
|
11
|
+
|
12
|
+
argument :password_confirmation, String, required: true do
|
13
|
+
description "User's new password confirmation"
|
14
|
+
end
|
15
|
+
|
16
|
+
field :errors, [::Types::Authentication::Error], null: false
|
17
|
+
field :success, Boolean, null: false
|
18
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
19
|
+
|
20
|
+
def resolve(args)
|
21
|
+
user = context[:current_user]
|
22
|
+
|
23
|
+
if user.blank?
|
24
|
+
return {
|
25
|
+
errors: [
|
26
|
+
{ field: :_error, message: I18n.t('devise.failure.unauthenticationenticated') }
|
27
|
+
],
|
28
|
+
success: false,
|
29
|
+
user: nil
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
user.update_with_password args
|
34
|
+
|
35
|
+
if user.errors.any?
|
36
|
+
{
|
37
|
+
errors: user.errors.messages.map do |field, messages|
|
38
|
+
{ field: field.to_s.camelize(:lower), message: messages.first.capitalize }
|
39
|
+
end,
|
40
|
+
success: false,
|
41
|
+
user: nil
|
42
|
+
}
|
43
|
+
else
|
44
|
+
{
|
45
|
+
errors: [],
|
46
|
+
success: true,
|
47
|
+
user: user
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Mutations::Authentication::ValidateToken < GraphQL::Schema::Mutation
|
4
|
+
include ::Graphql::AccountLockHelper
|
5
|
+
|
6
|
+
field :errors, [::Types::Authentication::Error], null: false
|
7
|
+
field :success, Boolean, null: false
|
8
|
+
field :user, GraphQL::Authentication.configuration.user_type.constantize, null: true
|
9
|
+
field :valid, Boolean, null: false
|
10
|
+
|
11
|
+
def resolve
|
12
|
+
user = context[:current_user]
|
13
|
+
|
14
|
+
if user.present? && !account_locked?(user)
|
15
|
+
{
|
16
|
+
errors: [],
|
17
|
+
success: true,
|
18
|
+
user: user,
|
19
|
+
valid: true
|
20
|
+
}
|
21
|
+
else
|
22
|
+
{
|
23
|
+
errors: [],
|
24
|
+
success: false,
|
25
|
+
user: nil,
|
26
|
+
valid: false
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Types::Authentication::Error < GraphQL::Schema::Object
|
4
|
+
description 'Form error'
|
5
|
+
|
6
|
+
field :field, String, null: false do
|
7
|
+
description 'Field of the error'
|
8
|
+
end
|
9
|
+
|
10
|
+
field :message, String, null: false do
|
11
|
+
description 'Error message'
|
12
|
+
end
|
13
|
+
|
14
|
+
field :details, GraphQL::Types::JSON, null: true do
|
15
|
+
description 'Error details'
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Types::Authentication::User < GraphQL::Schema::Object
|
4
|
+
description 'Data of a user'
|
5
|
+
|
6
|
+
field :id, ID, null: false do
|
7
|
+
description 'ID of the user'
|
8
|
+
end
|
9
|
+
|
10
|
+
field :email, String, null: false do
|
11
|
+
description 'Email address of the user'
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# implements GraphQLAuthentication in in Types::MutationType to access authentication mutations
|
2
|
+
|
3
|
+
module Types::GraphqlAuthentication
|
4
|
+
include GraphQL::Schema::Interface
|
5
|
+
|
6
|
+
field :sign_in, mutation: ::Mutations::Authentication::SignIn
|
7
|
+
|
8
|
+
if GraphQL::Authentication.configuration.allow_sign_up
|
9
|
+
field :sign_up, mutation: GraphQL::Authentication.configuration.sign_up_mutation.constantize
|
10
|
+
end
|
11
|
+
|
12
|
+
field :forgot_password, mutation: ::Mutations::Authentication::ForgotPassword
|
13
|
+
field :reset_password, mutation: ::Mutations::Authentication::ResetPassword
|
14
|
+
|
15
|
+
field :update_account, mutation: GraphQL::Authentication.configuration.update_account_mutation.constantize
|
16
|
+
|
17
|
+
field :validate_token, mutation: ::Mutations::Authentication::ValidateToken
|
18
|
+
|
19
|
+
if GraphQL::Authentication.configuration.allow_lock_account
|
20
|
+
field :lock_account, mutation: Mutations::Authentication::LockAccount
|
21
|
+
end
|
22
|
+
|
23
|
+
if GraphQL::Authentication.configuration.allow_unlock_account
|
24
|
+
field :unlock_account, mutation: Mutations::Authentication::UnlockAccount
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# include this helper in GraphqlController to use context method so that current_user will be available
|
2
|
+
#
|
3
|
+
# ::GraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
|
4
|
+
|
5
|
+
module Graphql
|
6
|
+
module AuthenticationHelper
|
7
|
+
include ::Graphql::AccountLockHelper
|
8
|
+
include ::Graphql::TokenHelper
|
9
|
+
|
10
|
+
def context
|
11
|
+
{
|
12
|
+
current_user: current_user,
|
13
|
+
response: response
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# set current user from Authorization header
|
18
|
+
def current_user
|
19
|
+
authorization_token = request.headers['Authorization']
|
20
|
+
return nil if authorization_token.nil?
|
21
|
+
|
22
|
+
decrypted_token = GraphQL::Authentication::JwtManager.decode(authorization_token)
|
23
|
+
user = User.find_by id: decrypted_token['user']
|
24
|
+
return nil if user.blank? || account_locked?(user)
|
25
|
+
|
26
|
+
# update token if user is found with token
|
27
|
+
generate_access_token(user, response)
|
28
|
+
|
29
|
+
user
|
30
|
+
|
31
|
+
# rescue expired Authorization header with RefreshToken header
|
32
|
+
rescue JWT::ExpiredSignature
|
33
|
+
refresh_token = request.headers['RefreshToken']
|
34
|
+
return nil if refresh_token.nil?
|
35
|
+
|
36
|
+
user = User.find_by refresh_token: refresh_token
|
37
|
+
return nil if user.blank? || account_locked?(user)
|
38
|
+
|
39
|
+
generate_access_token(user, response)
|
40
|
+
set_refresh_token(user, response)
|
41
|
+
|
42
|
+
user
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# include this helper in GraphqlController to use context method so that current_user will be available
|
2
|
+
#
|
3
|
+
# ::GraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
|
4
|
+
|
5
|
+
module Graphql
|
6
|
+
module TokenHelper
|
7
|
+
def generate_access_token(user, response)
|
8
|
+
token = GraphQL::Authentication::JwtManager.issue_with_expiration({ user: user.id }) # TODO use uuid
|
9
|
+
response.set_header 'Authorization', token
|
10
|
+
response.set_header 'Expires', GraphQL::Authentication::JwtManager.token_expiration(token)
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_refresh_token(user, response)
|
14
|
+
refresh_token = user.refresh_token.presence || GraphQL::Authentication::JwtManager.issue_without_expiration({ user: user.id })
|
15
|
+
user.update_column :refresh_token, refresh_token
|
16
|
+
response.set_header 'RefreshToken', refresh_token
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_current_user(user)
|
20
|
+
context[:current_user] = user
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_refresh_token(user)
|
24
|
+
user.update_column :refresh_token, nil if user.refresh_token.present?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<p>Hello <%= @resource.email %>!</p>
|
2
|
+
|
3
|
+
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
4
|
+
|
5
|
+
<p><%= link_to 'Change my password', GraphQL::Authentication::ResetPassword.url(@token) %></p>
|
6
|
+
|
7
|
+
<p>If you didn't request this, please ignore this email.</p>
|
8
|
+
<p>Your password won't change until you access the link above and create a new one.</p>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module GraphqlAuthentication
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def copy_configuration
|
7
|
+
template 'graphql_authentication.rb.erb', 'config/initializers/graphql_authentication.rb'
|
8
|
+
end
|
9
|
+
|
10
|
+
def rake_db
|
11
|
+
rake("railties:install:migrations")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
GraphQL::Authentication.configure do |config|
|
2
|
+
# config.token_lifespan = 4.hours
|
3
|
+
# config.jwt_secret_key = ENV['JWT_SECRET_KEY']
|
4
|
+
# config.app_url = ENV['APP_URL']
|
5
|
+
|
6
|
+
# config.user_type = '::Types::Authentication::User'
|
7
|
+
|
8
|
+
# Devise allowed actions
|
9
|
+
# Don't forget to enable the lockable setting in your Devise user model if you plan on using the lock_account feature
|
10
|
+
# config.allow_sign_up = true
|
11
|
+
# config.allow_lock_account = false
|
12
|
+
# config.allow_unlock_account = false
|
13
|
+
|
14
|
+
# Allow custom mutations for signup and update account
|
15
|
+
# config.sign_up_mutation = '::Mutations::Authentication::SignUp'
|
16
|
+
# config.update_account_mutation = '::Mutations::Authentication::UpdateAccount'
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Authentication
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :token_lifespan,
|
5
|
+
:jwt_secret_key,
|
6
|
+
:app_url,
|
7
|
+
:user_type,
|
8
|
+
:allow_sign_up,
|
9
|
+
:allow_lock_account,
|
10
|
+
:allow_unlock_account,
|
11
|
+
:sign_up_mutation,
|
12
|
+
:update_account_mutation
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@token_lifespan = 4.hours
|
16
|
+
@jwt_secret_key = ENV['JWT_SECRET_KEY']
|
17
|
+
@app_url = ENV['APP_URL']
|
18
|
+
|
19
|
+
@user_type = '::Types::Authentication::User'
|
20
|
+
|
21
|
+
# Devise allowed actions
|
22
|
+
@allow_sign_up = true
|
23
|
+
@allow_lock_account = false
|
24
|
+
@allow_unlock_account = false
|
25
|
+
|
26
|
+
# Allow custom mutations for signup and update account
|
27
|
+
@sign_up_mutation = '::Mutations::Authentication::SignUp'
|
28
|
+
@update_account_mutation = '::Mutations::Authentication::UpdateAccount'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
require 'graphql_authentication'
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Authentication
|
6
|
+
class JwtManager
|
7
|
+
ALGORITHM = 'HS256'
|
8
|
+
TYPE = 'Bearer'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def issue_with_expiration(payload, custom_expiration = nil)
|
12
|
+
if custom_expiration.present? && custom_expiration.is_a?(ActiveSupport::Duration)
|
13
|
+
payload[:exp] = custom_expiration
|
14
|
+
else
|
15
|
+
payload.merge!(expiration)
|
16
|
+
end
|
17
|
+
|
18
|
+
issue payload
|
19
|
+
end
|
20
|
+
|
21
|
+
def issue(payload)
|
22
|
+
token = JWT.encode payload,
|
23
|
+
authentication_secret,
|
24
|
+
ALGORITHM
|
25
|
+
set_type token
|
26
|
+
end
|
27
|
+
|
28
|
+
def token_expiration(token)
|
29
|
+
decrypted_token = decode(token)
|
30
|
+
decrypted_token.try(:[], 'exp')
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode(token)
|
34
|
+
token = extract_token token
|
35
|
+
decrypted_token = JWT.decode token,
|
36
|
+
authentication_secret,
|
37
|
+
true,
|
38
|
+
{ algorithm: ALGORITHM }
|
39
|
+
decrypted_token.first
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def authentication_secret
|
45
|
+
GraphQL::Authentication.configuration.jwt_secret_key
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_type(token)
|
49
|
+
"#{TYPE} #{token}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_token(token)
|
53
|
+
token.gsub "#{TYPE} ", ''
|
54
|
+
end
|
55
|
+
|
56
|
+
def expiration
|
57
|
+
exp = Time.now.to_i + GraphQL::Authentication.configuration.token_lifespan
|
58
|
+
{ exp: exp }
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :issue_without_expiration, :issue
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Authentication
|
3
|
+
class ResetPassword
|
4
|
+
class << self
|
5
|
+
def url(token)
|
6
|
+
url = I18n.locale === :fr ? 'nouveau-mot-de-passe' : 'new-password'
|
7
|
+
"#{GraphQL::Authentication.configuration.app_url}/#{I18n.locale}/#{url}/#{token}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'devise'
|
2
|
+
require 'graphql'
|
3
|
+
require 'graphql_authentication/configuration'
|
4
|
+
require 'graphql_authentication/engine'
|
5
|
+
require 'graphql_authentication/reset_password'
|
6
|
+
require 'graphql_authentication/jwt_manager'
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
module Authentication
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
@configuration ||= Configuration.new
|
16
|
+
yield(configuration)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql_authentication
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guillaume Ferland
|
8
|
+
- Brice Sanchez
|
9
|
+
- Guillaume Loubier
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2024-07-13 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: graphql
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: devise
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :runtime
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: jwt
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
type: :runtime
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: sqlite3
|
73
|
+
requirement: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: bundler
|
87
|
+
requirement: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - "~>"
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '2.0'
|
92
|
+
type: :development
|
93
|
+
prerelease: false
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - "~>"
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '2.0'
|
99
|
+
- !ruby/object:Gem::Dependency
|
100
|
+
name: rake
|
101
|
+
requirement: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - "~>"
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '10.0'
|
106
|
+
type: :development
|
107
|
+
prerelease: false
|
108
|
+
version_requirements: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - "~>"
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '10.0'
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: rspec
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '3.0'
|
120
|
+
type: :development
|
121
|
+
prerelease: false
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - "~>"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '3.0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: database_cleaner
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
description: GraphQL + JWT + Devise
|
142
|
+
email:
|
143
|
+
- ferland182@gmail.com
|
144
|
+
executables: []
|
145
|
+
extensions: []
|
146
|
+
extra_rdoc_files: []
|
147
|
+
files:
|
148
|
+
- CHANGELOG.md
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- app/graphql/mutations/authentication/forgot_password.rb
|
152
|
+
- app/graphql/mutations/authentication/lock_account.rb
|
153
|
+
- app/graphql/mutations/authentication/reset_password.rb
|
154
|
+
- app/graphql/mutations/authentication/sign_in.rb
|
155
|
+
- app/graphql/mutations/authentication/sign_up.rb
|
156
|
+
- app/graphql/mutations/authentication/unlock_account.rb
|
157
|
+
- app/graphql/mutations/authentication/update_account.rb
|
158
|
+
- app/graphql/mutations/authentication/validate_token.rb
|
159
|
+
- app/graphql/types/authentication/error.rb
|
160
|
+
- app/graphql/types/authentication/user.rb
|
161
|
+
- app/graphql/types/graphql_authentication.rb
|
162
|
+
- app/helpers/graphql/account_lock_helper.rb
|
163
|
+
- app/helpers/graphql/authentication_helper.rb
|
164
|
+
- app/helpers/graphql/token_helper.rb
|
165
|
+
- app/views/devise/mailer/reset_password_instructions.html.erb
|
166
|
+
- db/migrate/20190108151146_add_refresh_token_to_user.rb
|
167
|
+
- db/migrate/20190226175233_add_lockable_to_devise.rb
|
168
|
+
- lib/generators/graphql_authentication/install_generator.rb
|
169
|
+
- lib/generators/graphql_authentication/templates/graphql_authentication.rb.erb
|
170
|
+
- lib/graphql_authentication.rb
|
171
|
+
- lib/graphql_authentication/configuration.rb
|
172
|
+
- lib/graphql_authentication/engine.rb
|
173
|
+
- lib/graphql_authentication/jwt_manager.rb
|
174
|
+
- lib/graphql_authentication/reset_password.rb
|
175
|
+
- lib/graphql_authentication/version.rb
|
176
|
+
homepage: https://github.com/wbotelhos/graphql_authentication
|
177
|
+
licenses:
|
178
|
+
- MIT
|
179
|
+
metadata: {}
|
180
|
+
post_install_message:
|
181
|
+
rdoc_options: []
|
182
|
+
require_paths:
|
183
|
+
- lib
|
184
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: 2.4.5
|
189
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
requirements: []
|
195
|
+
rubygems_version: 3.5.15
|
196
|
+
signing_key:
|
197
|
+
specification_version: 4
|
198
|
+
summary: GraphQL + JWT + Devise
|
199
|
+
test_files: []
|