Class: OpenC3::AuthModel
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 seconds to keep redis values in memory
5- SESSION_CACHE_TIMEOUT =
5- MIN_PASSWORD_LENGTH =
8- SESSION_PREFIX =
"ses_"- OTP_PREFIX =
"otp_"- SESSION_TOKEN_REGEX =
A session token is SESSION_PREFIX followed by the unpadded urlsafe base64 encoding of 16 random bytes, i.e. exactly 22 characters of [A-Za-z0-9_-]. Keep this in sync with generate_session.
/\A#{SESSION_PREFIX}[A-Za-z0-9_-]{22}\z/- @@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
-
.generate_session(otp: false) ⇒ String
Creates a new session token.
-
.logout ⇒ Object
Terminates every session.
-
.session_token?(token) ⇒ Boolean
Whether the given value could have been produced by generate_session.
- .set(password, old_password, key = PRIMARY_KEY) ⇒ Object
- .set?(key = PRIMARY_KEY) ⇒ Boolean
-
.terminate(token) ⇒ Object
Terminates the given session token.
-
.terminate_otp(token) ⇒ Object
Terminates the session if the token is an OTP.
-
.verify(token, no_password: true, service_only: false) ⇒ Boolean
Checks whether the provided token is a valid user password, service password, or session token.
-
.verify_no_service(token, mode: :token) ⇒ Boolean
Checks whether the provided token is a valid user password or session token.
Class Method Details
.generate_session(otp: false) ⇒ String
Creates a new session token. DO NOT CALL BEFORE VERIFYING.
155 156 157 158 159 160 161 162 163 164 |
# File 'lib/openc3/models/auth_model.rb', line 155 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 |
.logout ⇒ Object
Terminates every session.
167 168 169 170 171 |
# File 'lib/openc3/models/auth_model.rb', line 167 def self.logout Store.del(SESSIONS_KEY) @@session_cache = nil @@session_cache_time = nil end |
.session_token?(token) ⇒ Boolean
Whether the given value could have been produced by generate_session. Callers handling a token from an unauthenticated request use this to reject garbage before doing any Redis work. Checking only the prefix is not enough: something like "ses_AAA" would pass and still cost us a read.
58 59 60 |
# File 'lib/openc3/models/auth_model.rb', line 58 def self.session_token?(token) SESSION_TOKEN_REGEX.match?(token.to_s) end |
.set(password, old_password, key = PRIMARY_KEY) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/openc3/models/auth_model.rb', line 137 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
62 63 64 |
# File 'lib/openc3/models/auth_model.rb', line 62 def self.set?(key = PRIMARY_KEY) Store.exists(key) == 1 end |
.terminate(token) ⇒ Object
Terminates the given session token.
174 175 176 177 |
# File 'lib/openc3/models/auth_model.rb', line 174 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.
180 181 182 |
# File 'lib/openc3/models/auth_model.rb', line 180 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.
71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/openc3/models/auth_model.rb', line 71 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.fetch('OPENC3_SERVICE_PASSWORD', nil) 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.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/openc3/models/auth_model.rb', line 87 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 # Drop the whole cache once it ages out rather than tracking per token # times. This bounds how long a terminated token keeps working to # SESSION_CACHE_TIMEOUT, same as before. if @@session_cache.nil? or (time - @@session_cache_time) >= SESSION_CACHE_TIMEOUT @@session_cache = {} @@session_cache_time = time end if @@session_cache[token] terminate_otp(token) return true end # Check the stored session tokens for just this token. Deliberately not # HGETALL: verify_token is unauthenticated, so reading the entire # session hash here lets any caller make us pull every session ever # created on every request, and that hash only grows. HGET is O(1) and # can't be amplified. The cache then fills with the tokens actually in # use instead of all of them. stored = Store.hget(SESSIONS_KEY, token) if stored @@session_cache[token] = stored 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 |