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.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +132 -0
  4. data/Rakefile +18 -0
  5. data/app/graphql/mutations/authentication/forgot_password.rb +29 -0
  6. data/app/graphql/mutations/authentication/lock_account.rb +31 -0
  7. data/app/graphql/mutations/authentication/reset_password.rb +48 -0
  8. data/app/graphql/mutations/authentication/sign_in.rb +58 -0
  9. data/app/graphql/mutations/authentication/sign_up.rb +44 -0
  10. data/app/graphql/mutations/authentication/unlock_account.rb +31 -0
  11. data/app/graphql/mutations/authentication/update_account.rb +51 -0
  12. data/app/graphql/mutations/authentication/validate_token.rb +30 -0
  13. data/app/graphql/types/authentication/error.rb +17 -0
  14. data/app/graphql/types/authentication/user.rb +13 -0
  15. data/app/graphql/types/graphql_authentication.rb +26 -0
  16. data/app/helpers/graphql/account_lock_helper.rb +14 -0
  17. data/app/helpers/graphql/authentication_helper.rb +45 -0
  18. data/app/helpers/graphql/token_helper.rb +27 -0
  19. data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
  20. data/db/migrate/20190108151146_add_refresh_token_to_user.rb +5 -0
  21. data/db/migrate/20190226175233_add_lockable_to_devise.rb +5 -0
  22. data/lib/generators/graphql_authentication/install_generator.rb +15 -0
  23. data/lib/generators/graphql_authentication/templates/graphql_authentication.rb.erb +17 -0
  24. data/lib/graphql_authentication/configuration.rb +32 -0
  25. data/lib/graphql_authentication/engine.rb +9 -0
  26. data/lib/graphql_authentication/jwt_manager.rb +65 -0
  27. data/lib/graphql_authentication/reset_password.rb +12 -0
  28. data/lib/graphql_authentication/version.rb +5 -0
  29. data/lib/graphql_authentication.rb +19 -0
  30. 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
+ [![Build Status](https://travis-ci.org/wbotelhos/graphql_authentication.svg?branch=master)](https://travis-ci.org/wbotelhos/graphql_authentication) [![Maintainability](https://api.codeclimate.com/v1/badges/7e2515bb59f0b205a603/maintainability)](https://codeclimate.com/github/wbotelhos/graphql_authentication/maintainability)
4
+ [![Downloads](https://img.shields.io/gem/dt/graphql_authentication.svg)](https://rubygems.org/gems/graphql_authentication)
5
+ [![Latest Version](https://img.shields.io/gem/v/graphql_authentication.svg)](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,14 @@
1
+ module Graphql
2
+ module AccountLockHelper
3
+
4
+ def account_locked?(user)
5
+ return false unless lockable?
6
+ user.access_locked?
7
+ end
8
+
9
+ def lockable?
10
+ GraphQL::Authentication.configuration.allow_lock_account
11
+ end
12
+
13
+ end
14
+ 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,5 @@
1
+ class AddRefreshTokenToUser < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :users, :refresh_token, :string, default: nil
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddLockableToDevise < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :users, :locked_at, :datetime
4
+ end
5
+ end
@@ -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,9 @@
1
+ module GraphQL
2
+ module Authentication
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace GraphQL::Authentication
5
+
6
+ config.autoload_paths += Dir["#{config.root}/app/**/*.rb"]
7
+ end
8
+ end
9
+ 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,5 @@
1
+ module GraphQL
2
+ module Authentication
3
+ VERSION = '1.0.0'
4
+ end
5
+ 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: []