linked_rails-auth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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')