Class: OpenC3::AuthModel

Inherits:
Object show all
Defined in:
lib/openc3/models/auth_model.rb

Constant Summary collapse

ARGON2_PROFILE =
ENV["OPENC3_ARGON2_PROFILE"]&.to_sym || :rfc_9106_low_memory
PRIMARY_KEY =

Redis keys

'OPENC3__TOKEN'
SESSIONS_KEY =

for argon2 password hash

'OPENC3__SESSIONS'
PW_HASH_CACHE_TIMEOUT =

The length of time in minutes to keep redis values in memory

5
SESSION_CACHE_TIMEOUT =
5
MIN_PASSWORD_LENGTH =
8
SESSION_PREFIX =
"ses_"
OTP_PREFIX =
"otp_"
@@pw_hash_cache =

Cached argon2 password hash

nil
@@pw_hash_cache_time =
nil
@@session_cache =

Cached session tokens

nil
@@session_cache_time =
nil

Class Method Summary collapse

Class Method Details

.generate_session(otp: false) ⇒ String

Creates a new session token. DO NOT CALL BEFORE VERIFYING.

Parameters:

  • otp (Boolean) (defaults to: false)

    whether to create a one-time use token (default: false)

Returns:

  • (String)

    the new session token



127
128
129
130
131
132
133
134
135
136
# File 'lib/openc3/models/auth_model.rb', line 127

def self.generate_session(otp: false)
  token = SecureRandom.urlsafe_base64(nil, false)
  if otp
    token = OTP_PREFIX + token
  else
    token = SESSION_PREFIX + token
  end
  Store.hset(SESSIONS_KEY, token, Time.now.iso8601)
  return token
end

.logoutObject

Terminates every session.



139
140
141
142
143
# File 'lib/openc3/models/auth_model.rb', line 139

def self.logout
  Store.del(SESSIONS_KEY)
  @@session_cache = nil
  @@session_cache_time = nil
end

.set(password, old_password, key = PRIMARY_KEY) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/openc3/models/auth_model.rb', line 109

def self.set(password, old_password, key = PRIMARY_KEY)
  raise "password must not be nil or empty" if password.nil? or password.empty?
  raise "password must be at least 8 characters" if password.length < MIN_PASSWORD_LENGTH

  if set?(key)
    raise "old_password must not be nil or empty" if old_password.nil? or old_password.empty?
    raise "old_password incorrect" unless verify_no_service(old_password, mode: :password)
  end
  pw_hash = Argon2::Password.create(password, profile: ARGON2_PROFILE)
  Store.set(key, pw_hash)
  @@pw_hash_cache = nil
  @@pw_hash_cache_time = nil
  logout
end

.set?(key = PRIMARY_KEY) ⇒ Boolean

Returns:

  • (Boolean)


47
48
49
# File 'lib/openc3/models/auth_model.rb', line 47

def self.set?(key = PRIMARY_KEY)
  Store.exists(key) == 1
end

.terminate(token) ⇒ Object

Terminates the given session token.



146
147
148
149
# File 'lib/openc3/models/auth_model.rb', line 146

def self.terminate(token)
  Store.hdel(SESSIONS_KEY, token)
  @@session_cache.delete(token) if @@session_cache
end

.terminate_otp(token) ⇒ Object

Terminates the session if the token is an OTP.



152
153
154
# File 'lib/openc3/models/auth_model.rb', line 152

def self.terminate_otp(token)
  terminate(token) if token.start_with?(OTP_PREFIX)
end

.verify(token, no_password: true, service_only: false) ⇒ Boolean

Checks whether the provided token is a valid user password, service password, or session token.

Parameters:

  • token (String)

    the plaintext password or session token to check (required)

  • no_password (Boolean) (defaults to: true)

    enforces use of a session token or service password (default: true)

  • service_only (Boolean) (defaults to: false)

    enforces use of a service password (default: false)

Returns:

  • (Boolean)

    whether the provided password/token is valid



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/openc3/models/auth_model.rb', line 56

def self.verify(token, no_password: true, service_only: false)
  # Handle a service password - Generally only used by ScriptRunner
  # TODO: Replace this with temporary service tokens
  service_password = ENV['OPENC3_SERVICE_PASSWORD']
  return true if service_password and service_password == token

  return false if service_only

  mode = no_password ? :token : :any
  return verify_no_service(token, mode: mode)
end

.verify_no_service(token, mode: :token) ⇒ Boolean

Checks whether the provided token is a valid user password or session token.

Parameters:

  • token (String)

    the plaintext password or session token to check (required)

  • mode (String) (defaults to: :token)

    optionally restrict verification to just the password or token. Valid values: :password, :token, or :any (default :token)

Returns:

  • (Boolean)

    whether the provided password/token is valid

Raises:

  • (ArgumentError)


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
# File 'lib/openc3/models/auth_model.rb', line 72

def self.verify_no_service(token, mode: :token)
  modes = [:password, :token, :any]
  raise ArgumentError, "Invalid mode '#{mode}': must be one of #{modes}" unless modes.include?(mode)

  return false if token.nil? or token.empty?

  # Check cached session tokens and password hash
  time = Time.now
  unless mode == :password
    if @@session_cache and (time - @@session_cache_time) < SESSION_CACHE_TIMEOUT and @@session_cache[token]
      terminate_otp(token)
      return true
    end

    # Check stored session tokens
    @@session_cache = Store.hgetall(SESSIONS_KEY)
    @@session_cache_time = time
    if @@session_cache[token]
      terminate_otp(token)
      return true
    end
  end

  unless mode == :token
    return true if @@pw_hash_cache and (time - @@pw_hash_cache_time) < PW_HASH_CACHE_TIMEOUT and Argon2::Password.verify_password(token, @@pw_hash_cache)

    # Check stored password hash
    pw_hash = Store.get(PRIMARY_KEY)
    raise "invalid password hash" if pw_hash.nil? || !pw_hash.start_with?("$argon2") # Catch users who didn't run the migration utility when upgrading to COSMOS 7
    @@pw_hash_cache = pw_hash
    @@pw_hash_cache_time = time
    return true if Argon2::Password.verify_password(token, @@pw_hash_cache)
  end

  return false
end