linked_rails-auth 0.0.1

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +28 -0
  4. data/Rakefile +34 -0
  5. data/app/actions/linked_rails/auth/access_token_action_list.rb +16 -0
  6. data/app/actions/linked_rails/auth/confirmation_action_list.rb +17 -0
  7. data/app/actions/linked_rails/auth/otp_attempt_action_list.rb +13 -0
  8. data/app/actions/linked_rails/auth/otp_secret_action_list.rb +31 -0
  9. data/app/actions/linked_rails/auth/password_action_list.rb +25 -0
  10. data/app/actions/linked_rails/auth/registration_action_list.rb +15 -0
  11. data/app/actions/linked_rails/auth/session_action_list.rb +22 -0
  12. data/app/actions/linked_rails/auth/unlock_action_list.rb +17 -0
  13. data/app/controllers/linked_rails/auth/access_tokens_controller.rb +131 -0
  14. data/app/controllers/linked_rails/auth/confirmations_controller.rb +87 -0
  15. data/app/controllers/linked_rails/auth/otp_attempts_controller.rb +21 -0
  16. data/app/controllers/linked_rails/auth/otp_secrets_controller.rb +40 -0
  17. data/app/controllers/linked_rails/auth/passwords_controller.rb +63 -0
  18. data/app/controllers/linked_rails/auth/registrations_controller.rb +33 -0
  19. data/app/controllers/linked_rails/auth/sessions_controller.rb +55 -0
  20. data/app/controllers/linked_rails/auth/unlocks_controller.rb +44 -0
  21. data/app/forms/linked_rails/auth/access_token_form.rb +20 -0
  22. data/app/forms/linked_rails/auth/confirmation_form.rb +9 -0
  23. data/app/forms/linked_rails/auth/otp_attempt_form.rb +9 -0
  24. data/app/forms/linked_rails/auth/otp_secret_form.rb +12 -0
  25. data/app/forms/linked_rails/auth/password_form.rb +21 -0
  26. data/app/forms/linked_rails/auth/registration_form.rb +21 -0
  27. data/app/forms/linked_rails/auth/session_form.rb +13 -0
  28. data/app/forms/linked_rails/auth/unlock_form.rb +9 -0
  29. data/app/helpers/linked_rails/auth/otp_helper.rb +30 -0
  30. data/app/models/linked_rails/auth/access_token.rb +40 -0
  31. data/app/models/linked_rails/auth/confirmation.rb +70 -0
  32. data/app/models/linked_rails/auth/guest_user.rb +29 -0
  33. data/app/models/linked_rails/auth/otp_attempt.rb +41 -0
  34. data/app/models/linked_rails/auth/otp_base.rb +57 -0
  35. data/app/models/linked_rails/auth/otp_secret.rb +91 -0
  36. data/app/models/linked_rails/auth/password.rb +47 -0
  37. data/app/models/linked_rails/auth/registration.rb +39 -0
  38. data/app/models/linked_rails/auth/session.rb +46 -0
  39. data/app/models/linked_rails/auth/unlock.rb +59 -0
  40. data/app/policies/linked_rails/auth/access_token_policy.rb +17 -0
  41. data/app/policies/linked_rails/auth/confirmation_policy.rb +17 -0
  42. data/app/policies/linked_rails/auth/otp_attempt_policy.rb +17 -0
  43. data/app/policies/linked_rails/auth/otp_secret_policy.rb +37 -0
  44. data/app/policies/linked_rails/auth/password_policy.rb +23 -0
  45. data/app/policies/linked_rails/auth/registration_policy.rb +13 -0
  46. data/app/policies/linked_rails/auth/session_policy.rb +17 -0
  47. data/app/policies/linked_rails/auth/unlock_policy.rb +21 -0
  48. data/app/serializers/linked_rails/auth/access_token_serializer.rb +11 -0
  49. data/app/serializers/linked_rails/auth/confirmation_serializer.rb +10 -0
  50. data/app/serializers/linked_rails/auth/otp_attempt_serializer.rb +12 -0
  51. data/app/serializers/linked_rails/auth/otp_secret_serializer.rb +14 -0
  52. data/app/serializers/linked_rails/auth/password_serializer.rb +18 -0
  53. data/app/serializers/linked_rails/auth/registration_serializer.rb +12 -0
  54. data/app/serializers/linked_rails/auth/session_serializer.rb +10 -0
  55. data/app/serializers/linked_rails/auth/unlock_serializer.rb +9 -0
  56. data/config/routes.rb +4 -0
  57. data/lib/generators/linked_rails/auth/install_generator.rb +155 -0
  58. data/lib/generators/linked_rails/auth/templates/README +2 -0
  59. data/lib/generators/linked_rails/auth/templates/doorkeeper_jwt_initializer.rb +52 -0
  60. data/lib/generators/linked_rails/auth/templates/locales.yml +39 -0
  61. data/lib/generators/linked_rails/auth/templates/migration.rb.erb +20 -0
  62. data/lib/linked_rails/auth/auth_helper.rb +101 -0
  63. data/lib/linked_rails/auth/controller/error_handling.rb +17 -0
  64. data/lib/linked_rails/auth/controller.rb +16 -0
  65. data/lib/linked_rails/auth/engine.rb +8 -0
  66. data/lib/linked_rails/auth/errors/account_locked.rb +10 -0
  67. data/lib/linked_rails/auth/errors/expired.rb +10 -0
  68. data/lib/linked_rails/auth/errors/unauthorized.rb +10 -0
  69. data/lib/linked_rails/auth/errors/unknown_email.rb +14 -0
  70. data/lib/linked_rails/auth/errors/wrong_password.rb +14 -0
  71. data/lib/linked_rails/auth/errors.rb +7 -0
  72. data/lib/linked_rails/auth/routes.rb +86 -0
  73. data/lib/linked_rails/auth/version.rb +7 -0
  74. data/lib/linked_rails/auth.rb +46 -0
  75. data/lib/tasks/linked_rails/auth_tasks.rake +6 -0
  76. metadata +257 -0
@@ -0,0 +1,2 @@
1
+ You need to set your JWT encryption token.
2
+ Please run `bin/rails credentials:edit` and enter `jwt_encryption_token: [SECRET]``
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ Doorkeeper::JWT.configure do
4
+ # Set the payload for the JWT token. This should contain unique information
5
+ # about the user.
6
+ # Defaults to a randomly generated token in a hash
7
+ # { token: "RANDOM-TOKEN" }
8
+ token_payload do |opts|
9
+ user =
10
+ if opts[:scopes].include?('guest')
11
+ LinkedRails.guest_user_class.new(id: opts[:resource_owner_id])
12
+ elsif opts[:resource_owner_id]
13
+ User.find(opts[:resource_owner_id])
14
+ end
15
+
16
+ user_opts = user && {
17
+ type: user.guest? ? 'guest' : 'user',
18
+ '@id': user.iri,
19
+ id: user.id.to_s,
20
+ language: I18n.locale
21
+ }
22
+ payload = {
23
+ iat: Time.current.to_i,
24
+ iss: LinkedRails.iri.to_s,
25
+ application_id: opts[:application]&.uid,
26
+ scopes: opts[:scopes].entries,
27
+ user: user_opts
28
+ }
29
+ payload[:exp] = (opts[:created_at] + opts[:expires_in].seconds).to_i if opts[:expires_in].present?
30
+ payload
31
+ end
32
+
33
+ # Use the application secret specified in the Access Grant token
34
+ # Defaults to false
35
+ # If you specify `use_application_secret true`, both secret_key and secret_key_path will be ignored
36
+ # use_application_secret true
37
+
38
+ # Set the encryption secret. This would be shared with any other applications
39
+ # that should be able to read the payload of the token.
40
+ # Defaults to "secret"
41
+ secret_key Rails.application.secrets.jwt_encryption_token || Rails.application.credentials.jwt_encryption_token
42
+
43
+ # If you want to use RS* encoding specify the path to the RSA key
44
+ # to use for signing.
45
+ # If you specify a secret_key_path it will be used instead of secret_key
46
+ # secret_key_path 'path/to/file.pem'
47
+
48
+ # Specify encryption type. Supports any algorithim in
49
+ # https://github.com/progrium/ruby-jwt
50
+ # defaults to nil
51
+ encryption_method Rails.application.config.jwt_encryption_method
52
+ end
@@ -0,0 +1,39 @@
1
+ en:
2
+ actions:
3
+ confirmations:
4
+ create:
5
+ label: "Send confirmation link again"
6
+ submit: "Send"
7
+ update:
8
+ label: "Confirm email address"
9
+ submit: "Confirm"
10
+ otp_attempts:
11
+ create:
12
+ submit: "Continue"
13
+ otp_secrets:
14
+ create:
15
+ label: 'Two factor authentication'
16
+ submit: "Continue"
17
+ destroy:
18
+ label: 'Disable two factor authentication'
19
+ description: "Are you sure you want to disable the two factor authentication of **%{name}**?"
20
+ submit: "Confirm"
21
+ success: "Two factor authentication is disabled"
22
+ devise:
23
+ failure:
24
+ invalid_email: 'We couldn''t find a user with this email.'
25
+ invalid_password: 'Invalid password.'
26
+ forms:
27
+ access_tokens:
28
+ email:
29
+ label: "Email"
30
+ otp_attempts:
31
+ helper_text: 'You can find this in the authentication-application you used earlier.'
32
+ label: "Authentication code"
33
+ otp_secrets:
34
+ provision_image:
35
+ description: 'Install a authentication-application and scan this QR code.'
36
+ sessions:
37
+ email:
38
+ label: "Email"
39
+
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDoorkeeperApp < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ create_table :otp_secrets do |t|
6
+ t.timestamps
7
+ t.integer :owner_id, null: false
8
+ t.string :otp_secret_key, null: false
9
+ t.boolean :active, default: false
10
+ t.index :owner_id, unique: true
11
+ end
12
+ end
13
+
14
+ def down
15
+ Doorkeeper::AccessToken.delete_all
16
+ Doorkeeper::Application.delete_all
17
+
18
+ drop_table :otp_secrets
19
+ end
20
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module AuthHelper
6
+ include Doorkeeper::Rails::Helpers
7
+ include Doorkeeper::Helpers::Controller
8
+
9
+ SAFE_METHODS = %w[GET HEAD OPTIONS CONNECT TRACE].freeze
10
+ UNSAFE_METHODS = %w[POST PUT PATCH DELETE].freeze
11
+
12
+ def current_user
13
+ return request.env['Current-User'] if request.env['Current-User']
14
+ return @current_user if instance_variable_defined?(:@current_user)
15
+
16
+ @current_user ||= current_resource_owner || create_guest_user
17
+
18
+ handle_invalid_token unless valid_token?
19
+
20
+ @current_user
21
+ end
22
+
23
+ def doorkeeper_token
24
+ request.env['Doorkeeper-Token'] || super
25
+ end
26
+
27
+ private
28
+
29
+ def create_guest_user
30
+ LinkedRails.guest_user_class.new
31
+ end
32
+
33
+ def doorkeeper_scopes
34
+ doorkeeper_token&.scopes || []
35
+ end
36
+
37
+ def doorkeeper_token_payload
38
+ @doorkeeper_token_payload ||= JWT.decode(
39
+ doorkeeper_token.token,
40
+ Doorkeeper::JWT.configuration.secret_key,
41
+ true,
42
+ algorithms: [Doorkeeper::JWT.configuration.encryption_method.to_s.upcase]
43
+ )[0]
44
+ end
45
+
46
+ def doorkeeper_unauthorized_render_options(error: nil)
47
+ {
48
+ json: {
49
+ error: :invalid_token,
50
+ error_description: error&.description
51
+ }
52
+ }
53
+ end
54
+
55
+ def generate_access_token(resource_owner)
56
+ Doorkeeper::AccessToken.find_or_create_for(
57
+ application: doorkeeper_token&.application,
58
+ resource_owner: resource_owner,
59
+ scopes: resource_owner.guest? ? :guest : :user,
60
+ expires_in: Doorkeeper.configuration.access_token_expires_in,
61
+ use_refresh_token: true
62
+ )
63
+ end
64
+
65
+ def handle_invalid_token
66
+ @current_user = create_guest_user
67
+ end
68
+
69
+ def sign_in(resource, *_args)
70
+ @current_user = resource
71
+ update_oauth_token(generate_access_token(resource))
72
+
73
+ return if request.env['warden'].blank? || warden.user(:user) == resource
74
+
75
+ warden.set_user(resource, scope: :user, store: false)
76
+ end
77
+
78
+ def sign_out(*args)
79
+ super
80
+
81
+ doorkeeper_token.revoke if doorkeeper_token&.resource_owner_id
82
+ update_oauth_token(generate_access_token(create_guest_user))
83
+ end
84
+
85
+ def update_oauth_token(token)
86
+ response.headers['New-Refresh-Token'] = token.refresh_token
87
+ response.headers['New-Authorization'] = token.token
88
+ end
89
+
90
+ def require_doorkeeper_token?
91
+ UNSAFE_METHODS.include?(request.method)
92
+ end
93
+
94
+ def valid_token?
95
+ return !require_doorkeeper_token? if doorkeeper_token.blank?
96
+
97
+ doorkeeper_token&.accessible?
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Controller
6
+ module ErrorHandling
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ rescue_from LinkedRails::Auth::Errors::Unauthorized, with: :handle_error
11
+ rescue_from LinkedRails::Auth::Errors::UnknownEmail, with: :handle_error
12
+ rescue_from LinkedRails::Auth::Errors::Expired, with: :handle_error
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'controller/error_handling'
4
+
5
+ module LinkedRails
6
+ module Auth
7
+ module Controller
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include LinkedRails::Auth::AuthHelper
12
+ include LinkedRails::Auth::Controller::ErrorHandling
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Errors
6
+ class AccountLocked < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Errors
6
+ class Expired < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Errors
6
+ class Unauthorized < StandardError
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Errors
6
+ class UnknownEmail < Doorkeeper::Errors::InvalidGrantReuse
7
+ def initialize(_options = {})
8
+ message = I18n.t('devise.failure.invalid_email')
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Errors
6
+ class WrongPassword < Doorkeeper::Errors::InvalidGrantReuse
7
+ def initialize(_options = {})
8
+ message = I18n.t('devise.failure.invalid_password')
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/account_locked'
4
+ require_relative 'errors/expired'
5
+ require_relative 'errors/unauthorized'
6
+ require_relative 'errors/unknown_email'
7
+ require_relative 'errors/wrong_password'
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ module Routes
6
+ DOORKEEPER_CONTROLLERS = {
7
+ access_tokens: :tokens,
8
+ applications: :applications,
9
+ authorizations: :authorizations,
10
+ authorized_applications: :authorized_applications,
11
+ token_info: :token_info
12
+ }.freeze
13
+ DEVISE_CONTROLLERS = %i[omniauth_callbacks].freeze
14
+ LINKED_RAILS_CONTROLLERS = {
15
+ access_tokens: 'linked_rails/auth/access_tokens',
16
+ confirmations: 'linked_rails/auth/confirmations',
17
+ otp_attempts: 'linked_rails/auth/otp_attempts',
18
+ otp_secrets: 'linked_rails/auth/otp_secrets',
19
+ passwords: 'linked_rails/auth/passwords',
20
+ registrations: 'linked_rails/auth/registrations',
21
+ sessions: 'linked_rails/auth/sessions',
22
+ unlocks: 'linked_rails/auth/unlocks'
23
+ }.freeze
24
+
25
+ def use_linked_rails_auth(opts = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
+ linked_rails_doorkeeper_routes(opts)
27
+ linked_rails_device_routes(opts)
28
+
29
+ scope 'u' do
30
+ get 'sign_in', to: redirect('/u/session/new')
31
+ end
32
+ devise_scope :user do
33
+ auth_resource(AccessToken, opts)
34
+ auth_resource(Confirmation, opts)
35
+ auth_resource(OtpAttempt, opts)
36
+ auth_resource(OtpSecret, opts)
37
+ linked_resource(
38
+ OtpSecret,
39
+ controller: opts[:otp_secrets] || LINKED_RAILS_CONTROLLERS[:otp_secrets],
40
+ nested: false
41
+ )
42
+ auth_resource(Password, opts)
43
+ auth_resource(Registration, opts)
44
+ auth_resource(Session, opts)
45
+ auth_resource(Unlock, opts)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def auth_resource(klass, opts)
52
+ key = klass.name.demodulize.tableize.to_sym
53
+
54
+ singular_linked_resource(
55
+ klass,
56
+ controller: opts[key] || LINKED_RAILS_CONTROLLERS[key],
57
+ nested: false
58
+ )
59
+ end
60
+
61
+ def linked_rails_device_routes(opts)
62
+ devise_for(
63
+ opts[:devise_scope] || :users,
64
+ path: :u,
65
+ controllers: {
66
+ omniauth_callbacks: opts[:omniauth_callbacks]
67
+ },
68
+ only: %i[omniauth_callbacks]
69
+ )
70
+ end
71
+
72
+ def linked_rails_doorkeeper_routes(opts)
73
+ use_doorkeeper do
74
+ DOORKEEPER_CONTROLLERS.each do |linked_rails_key, doorkeeper_key|
75
+ if opts.key?(linked_rails_key)
76
+ controllers(doorkeeper_key => opts[linked_rails_key])
77
+ elsif LINKED_RAILS_CONTROLLERS.key?(linked_rails_key)
78
+ controllers(doorkeeper_key => LINKED_RAILS_CONTROLLERS[linked_rails_key])
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ ActionDispatch::Routing::Mapper.include LinkedRails::Auth::Routes
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinkedRails
4
+ module Auth
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model_otp'
4
+ require 'devise'
5
+ require 'doorkeeper'
6
+ require 'doorkeeper/jwt'
7
+
8
+ require 'linked_rails/auth/engine'
9
+ require 'linked_rails/auth/routes'
10
+ require 'linked_rails/auth/errors'
11
+ require 'linked_rails/auth/auth_helper'
12
+ require 'linked_rails/auth/controller'
13
+
14
+ module LinkedRails
15
+ module Auth
16
+ mattr_accessor :otp_drift, default: 60
17
+ end
18
+ end
19
+
20
+ LinkedRails.configurable_class(nil, :user, default: 'User')
21
+ LinkedRails.configurable_class(nil, :guest_user, default: 'LinkedRails::Auth::GuestUser')
22
+ LinkedRails.configurable_class(nil, :access_token, default: 'LinkedRails::Auth::AccessToken')
23
+ LinkedRails.configurable_class(nil, :access_token_action_list, default: 'LinkedRails::Auth::AccessTokenActionList')
24
+ LinkedRails.configurable_class(nil, :access_token_form, default: 'LinkedRails::Auth::AccessTokenForm')
25
+ LinkedRails.configurable_class(nil, :confirmation, default: 'LinkedRails::Auth::Confirmation')
26
+ LinkedRails.configurable_class(nil, :confirmation_action_list, default: 'LinkedRails::Auth::ConfirmationActionList')
27
+ LinkedRails.configurable_class(nil, :confirmation_form, default: 'LinkedRails::Auth::ConfirmationForm')
28
+ LinkedRails.configurable_class(nil, :password, default: 'LinkedRails::Auth::Password')
29
+ LinkedRails.configurable_class(nil, :password_action_list, default: 'LinkedRails::Auth::PasswordActionList')
30
+ LinkedRails.configurable_class(nil, :password_form, default: 'LinkedRails::Auth::PasswordForm')
31
+ LinkedRails.configurable_class(nil, :registration, default: 'LinkedRails::Auth::Registration')
32
+ LinkedRails.configurable_class(nil, :registration_action_list, default: 'LinkedRails::Auth::RegistrationActionList')
33
+ LinkedRails.configurable_class(nil, :registration_form, default: 'LinkedRails::Auth::RegistrationForm')
34
+ LinkedRails.configurable_class(nil, :session, default: 'LinkedRails::Auth::Session')
35
+ LinkedRails.configurable_class(nil, :session_action_list, default: 'LinkedRails::Auth::SessionActionList')
36
+ LinkedRails.configurable_class(nil, :session_form, default: 'LinkedRails::Auth::SessionForm')
37
+ LinkedRails.configurable_class(nil, :unlock, default: 'LinkedRails::Auth::Unlock')
38
+ LinkedRails.configurable_class(nil, :unlock_action_list, default: 'LinkedRails::Auth::UnlockActionList')
39
+ LinkedRails.configurable_class(nil, :unlock_form, default: 'LinkedRails::Auth::UnlockForm')
40
+ LinkedRails.configurable_class(nil, :otp_attempt, default: 'LinkedRails::Auth::OtpAttempt')
41
+ LinkedRails.configurable_class(nil, :otp_attempt_action_list, default: 'LinkedRails::Auth::OtpAttemptActionList')
42
+ LinkedRails.configurable_class(nil, :otp_attempt_form, default: 'LinkedRails::Auth::OtpAttemptForm')
43
+ LinkedRails.configurable_class(nil, :otp_owner, default: 'User')
44
+ LinkedRails.configurable_class(nil, :otp_secret, default: 'LinkedRails::Auth::OtpSecret')
45
+ LinkedRails.configurable_class(nil, :otp_secret_action_list, default: 'LinkedRails::Auth::OtpSecretActionList')
46
+ LinkedRails.configurable_class(nil, :otp_secret_form, default: 'LinkedRails::Auth::OtpSecretForm')