Module: Devise::Models::Lockable

Extended by:
ActiveSupport::Concern
Defined in:
lib/devise/models/lockable.rb

Overview

Handles blocking a user access after a certain number of attempts. Lockable accepts two different strategies to unlock a user after it's blocked: email and time. The former will send an email to the user when the lock happens, containing a link to unlock its account. The second will unlock the user automatically after some configured time (ie 2.hours). It's also possible to setup lockable to use both email and time strategies.

Options

Lockable adds the following options to devise:

* +maximum_attempts+: how many attempts should be accepted before blocking the user.
* +lock_strategy+: lock the user account by :failed_attempts or :none.
* +unlock_strategy+: unlock the user account by :time, :email, :both or :none.
* +unlock_in+: the time you want to lock the user after to lock happens. Only available when unlock_strategy is :time or :both.
* +unlock_keys+: the keys you want to use when locking and unlocking an account

Defined Under Namespace

Modules: ClassMethods

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Object) required_fields(klass)



27
28
29
30
31
32
33
34
# File 'lib/devise/models/lockable.rb', line 27

def self.required_fields(klass)
  attributes = []
  attributes << :failed_attempts if klass.lock_strategy_enabled?(:failed_attempts)
  attributes << :locked_at if klass.unlock_strategy_enabled?(:time)
  attributes << :unlock_token if klass.unlock_strategy_enabled?(:email)

  attributes
end

Instance Method Details

- (Boolean) access_locked?

Verifies whether a user is locked or not.

Returns:

  • (Boolean)


59
60
61
# File 'lib/devise/models/lockable.rb', line 59

def access_locked?
  !!locked_at && !lock_expired?
end

- (Boolean) active_for_authentication?

Overwrites active_for_authentication? from Devise::Models::Activatable for locking purposes by verifying whether a user is active to sign in or not based on locked?

Returns:

  • (Boolean)


79
80
81
# File 'lib/devise/models/lockable.rb', line 79

def active_for_authentication?
  super && !access_locked?
end

- (Boolean) attempts_exceeded? (protected)

Returns:

  • (Boolean)


129
130
131
# File 'lib/devise/models/lockable.rb', line 129

def attempts_exceeded?
  self.failed_attempts >= self.class.maximum_attempts
end

- (Object) if_access_locked (protected)

Checks whether the record is locked or not, yielding to the block if it's locked, otherwise adds an error to email.



148
149
150
151
152
153
154
155
# File 'lib/devise/models/lockable.rb', line 148

def if_access_locked
  if access_locked?
    yield
  else
    self.errors.add(Devise.unlock_keys.first, :not_locked)
    false
  end
end

- (Object) inactive_message

Overwrites invalid_message from Devise::Models::Authenticatable to define the correct reason for blocking the sign in.



85
86
87
# File 'lib/devise/models/lockable.rb', line 85

def inactive_message
  access_locked? ? :locked : super
end

- (Boolean) last_attempt? (protected)

Returns:

  • (Boolean)


133
134
135
# File 'lib/devise/models/lockable.rb', line 133

def last_attempt?
  self.failed_attempts == self.class.maximum_attempts - 1
end

- (Object) lock_access!(opts = { })

Lock a user setting its locked_at to actual time.

  • opts: Hash options if you don't want to send email when you lock access, you could pass the next hash `{ send_instructions: false } as option`.



40
41
42
43
44
45
46
47
48
# File 'lib/devise/models/lockable.rb', line 40

def lock_access!(opts = { })
  self.locked_at = Time.now.utc

  if unlock_strategy_enabled?(:email) && opts.fetch(:send_instructions, true)
    send_unlock_instructions
  else
    save(validate: false)
  end
end

- (Boolean) lock_expired? (protected)

Tells if the lock is expired if :time unlock strategy is active

Returns:

  • (Boolean)


138
139
140
141
142
143
144
# File 'lib/devise/models/lockable.rb', line 138

def lock_expired?
  if unlock_strategy_enabled?(:time)
    locked_at && locked_at < self.class.unlock_in.ago
  else
    false
  end
end

- (Object) resend_unlock_instructions

Resend the unlock instructions if the user is locked.



73
74
75
# File 'lib/devise/models/lockable.rb', line 73

def resend_unlock_instructions
  if_access_locked { send_unlock_instructions }
end

- (Object) send_unlock_instructions

Send unlock instructions by email



64
65
66
67
68
69
70
# File 'lib/devise/models/lockable.rb', line 64

def send_unlock_instructions
  raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
  self.unlock_token = enc
  self.save(validate: false)
  send_devise_notification(:unlock_instructions, raw, {})
  raw
end

- (Object) unauthenticated_message



113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/devise/models/lockable.rb', line 113

def unauthenticated_message
  # If set to paranoid mode, do not show the locked message because it
  # leaks the existence of an account.
  if Devise.paranoid
    super
  elsif lock_strategy_enabled?(:failed_attempts) && last_attempt?
    :last_attempt
  elsif lock_strategy_enabled?(:failed_attempts) && attempts_exceeded?
    :locked
  else
    super
  end
end

- (Object) unlock_access!

Unlock a user by cleaning locked_at and failed_attempts.



51
52
53
54
55
56
# File 'lib/devise/models/lockable.rb', line 51

def unlock_access!
  self.locked_at = nil
  self.failed_attempts = 0 if respond_to?(:failed_attempts=)
  self.unlock_token = nil  if respond_to?(:unlock_token=)
  save(validate: false)
end

- (Boolean) valid_for_authentication?

Overwrites valid_for_authentication? from Devise::Models::Authenticatable for verifying whether a user is allowed to sign in or not. If the user is locked, it should never be allowed.

Returns:

  • (Boolean)


92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/devise/models/lockable.rb', line 92

def valid_for_authentication?
  return super unless persisted? && lock_strategy_enabled?(:failed_attempts)

  # Unlock the user if the lock is expired, no matter
  # if the user can login or not (wrong password, etc)
  unlock_access! if lock_expired?

  if super && !access_locked?
    true
  else
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      lock_access! unless access_locked?
    else
      save(validate: false)
    end
    false
  end
end