File: single_session.rb

package info (click to toggle)
ruby-rodauth 2.42.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 812 kB
  • sloc: ruby: 7,524; javascript: 100; makefile: 4
file content (109 lines) | stat: -rw-r--r-- 3,386 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# frozen-string-literal: true

module Rodauth
  Feature.define(:single_session, :SingleSession) do
    error_flash 'This session has been logged out as another session has become active'
    redirect

    auth_value_method :allow_raw_single_session_key?, false
    auth_value_method :inactive_session_error_status, 401
    auth_value_method :single_session_id_column, :id
    auth_value_method :single_session_key_column, :key
    session_key :single_session_session_key, :single_session_key
    auth_value_method :single_session_table, :account_session_keys

    auth_methods(
      :currently_active_session?,
      :no_longer_active_session,
      :reset_single_session_key,
      :update_single_session_key
    )

    def reset_single_session_key
      if logged_in?
        single_session_ds.update(single_session_key_column=>random_key)
      end
    end

    def currently_active_session?
      single_session_key = session[single_session_session_key]
      current_key = single_session_ds.get(single_session_key_column)
      if single_session_key.nil?
        unless current_key
          # No row exists for this user, indicating the feature has never
          # been used, so it is OK to treat the current session as a new
          # session.
          update_single_session_key
        end
        true
      elsif current_key
        if hmac_secret && !(valid = timing_safe_eql?(single_session_key, hmac = compute_hmac(current_key)))
          if hmac_secret_rotation? && (valid = timing_safe_eql?(single_session_key, compute_old_hmac(current_key)))
            session[single_session_session_key] = hmac
          elsif !allow_raw_single_session_key?
            return false
          end
        end

        valid || timing_safe_eql?(single_session_key, current_key)
      end
    end

    def check_single_session
      if logged_in? && !currently_active_session?
        no_longer_active_session
      end
    end

    def no_longer_active_session
      clear_session
      set_redirect_error_status inactive_session_error_status
      set_error_reason :inactive_session
      set_redirect_error_flash single_session_error_flash
      redirect single_session_redirect
    end

    def update_single_session_key
      key = random_key
      set_single_session_key(key)
      if single_session_ds.update(single_session_key_column=>key) == 0
        # Don't handle uniqueness violations here.  While we could get the stored key from the
        # database, it could lead to two sessions sharing the same key, which this feature is
        # designed to prevent.
        single_session_ds.insert(single_session_id_column=>session_value, single_session_key_column=>key)
      end
    end

    def update_session
      super
      update_single_session_key
    end

    def clear_tokens(reason)
      super
      single_session_ds(account_id).delete unless logged_in?
    end

    private

    def after_close_account
      super if defined?(super)
      single_session_ds.delete
    end

    def before_logout
      reset_single_session_key
      super if defined?(super)
    end

    def set_single_session_key(data)
      data = compute_hmac(data) if hmac_secret
      set_session_value(single_session_session_key, data)
    end

    def single_session_ds(id=session_value)
      db[single_session_table].
        where(single_session_id_column=>id)
    end
  end
end