diff --git a/openc3-cosmos-cmd-tlm-api/app/controllers/auth_controller.rb b/openc3-cosmos-cmd-tlm-api/app/controllers/auth_controller.rb index fe7c49110..4177f70d6 100644 --- a/openc3-cosmos-cmd-tlm-api/app/controllers/auth_controller.rb +++ b/openc3-cosmos-cmd-tlm-api/app/controllers/auth_controller.rb @@ -34,11 +34,12 @@ def token_exists def verify begin if OpenC3::AuthModel.verify(params[:token]) - head :ok + render :plain => OpenC3::AuthModel.generate_session() else head :unauthorized end rescue StandardError => e + OpenC3::Logger.error(e.formatted) render :json => { :status => 'error', :message => e.message, 'type' => e.class }, :status => 500 end end @@ -48,8 +49,9 @@ def set # Set throws an exception if it fails for any reason OpenC3::AuthModel.set(params[:token], params[:old_token]) OpenC3::Logger.info("Password changed", user: username()) - head :ok + render :plain => OpenC3::AuthModel.generate_session() rescue StandardError => e + OpenC3::Logger.error(e.formatted) render :json => { :status => 'error', :message => e.message, 'type' => e.class }, :status => 500 end end diff --git a/openc3-cosmos-cmd-tlm-api/app/controllers/users_controller.rb b/openc3-cosmos-cmd-tlm-api/app/controllers/users_controller.rb index 67846edb3..6581f9018 100644 --- a/openc3-cosmos-cmd-tlm-api/app/controllers/users_controller.rb +++ b/openc3-cosmos-cmd-tlm-api/app/controllers/users_controller.rb @@ -26,6 +26,7 @@ def active() end def logout() + OpenC3::AuthModel.logout head :ok end end diff --git a/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/base/components/Login.vue b/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/base/components/Login.vue index 62480c054..c4e017438 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/base/components/Login.vue +++ b/openc3-cosmos-init/plugins/packages/openc3-tool-common/src/tools/base/components/Login.vue @@ -140,12 +140,12 @@ export default { showReset: function () { this.reset = true }, - login: function () { - localStorage.openc3Token = this.password + login: function (response) { + localStorage.openc3Token = response.data const redirect = new URLSearchParams(window.location.search).get( 'redirect', ) - if (redirect[0] === '/' && redirect[1] !== '/') { + if (redirect.startsWith('/tools/')) { // Valid relative redirect URL window.location = decodeURI(redirect) } else { @@ -161,7 +161,7 @@ export default { ...this.options, }) .then((response) => { - this.login() + this.login(response) }) .catch((error) => { this.alert = 'Incorrect password' @@ -177,7 +177,9 @@ export default { token: this.password, }, ...this.options, - }).then(this.login) + }).then((response) => { + this.login(response) + }) }, }, } diff --git a/openc3-cosmos-script-runner-api/app/controllers/scripts_controller.rb b/openc3-cosmos-script-runner-api/app/controllers/scripts_controller.rb index e4393590a..7d1bf9a10 100644 --- a/openc3-cosmos-script-runner-api/app/controllers/scripts_controller.rb +++ b/openc3-cosmos-script-runner-api/app/controllers/scripts_controller.rb @@ -56,7 +56,7 @@ def delete_temp def body return unless authorization('script_view') - scope, name = sanitize_params([:scope, :name]) + scope, name = sanitize_params([:scope, :name], :allow_forward_slash => true) return unless scope file = Script.body(scope, name) @@ -126,7 +126,7 @@ def run def lock return unless authorization('script_edit') - scope, name = sanitize_params([:scope, :name]) + scope, name = sanitize_params([:scope, :name], :allow_forward_slash => true) return unless scope Script.lock(scope, name, username()) render status: 200 @@ -134,7 +134,7 @@ def lock def unlock return unless authorization('script_edit') - scope, name = sanitize_params([:scope, :name]) + scope, name = sanitize_params([:scope, :name], :allow_forward_slash => true) return unless scope locked_by = Script.locked?(scope, name) Script.unlock(scope, name) if username() == locked_by @@ -143,7 +143,7 @@ def unlock def destroy return unless authorization('script_edit') - scope, name = sanitize_params([:scope, :name]) + scope, name = sanitize_params([:scope, :name], :allow_forward_slash => true) return unless scope Script.destroy(scope, name) OpenC3::Logger.info("Script destroyed: #{name}", scope: scope, user: username()) diff --git a/openc3/lib/openc3/models/auth_model.rb b/openc3/lib/openc3/models/auth_model.rb index b7008582a..0a3414ab8 100644 --- a/openc3/lib/openc3/models/auth_model.rb +++ b/openc3/lib/openc3/models/auth_model.rb @@ -21,15 +21,22 @@ # if purchased from OpenC3, Inc. require 'digest' +require 'securerandom' require 'openc3/utilities/store' module OpenC3 class AuthModel PRIMARY_KEY = 'OPENC3__TOKEN' + SESSIONS_KEY = 'OPENC3__SESSIONS' TOKEN_CACHE_TIMEOUT = 5 + SESSION_CACHE_TIMEOUT = 5 @@token_cache = nil @@token_cache_time = nil + @@session_cache = nil + @@session_cache_time = nil + + MIN_TOKEN_LENGTH = 8 def self.set?(key = PRIMARY_KEY) Store.exists(key) == 1 @@ -38,14 +45,23 @@ def self.set?(key = PRIMARY_KEY) def self.verify(token) return false if token.nil? or token.empty? + time = Time.now + return true if @@session_cache and (time - @@session_cache_time) < SESSION_CACHE_TIMEOUT and @@session_cache[token] token_hash = hash(token) - return true if @@token_cache and (Time.now - @@token_cache_time) < TOKEN_CACHE_TIMEOUT and @@token_cache == token_hash + return true if @@token_cache and (time - @@token_cache_time) < TOKEN_CACHE_TIMEOUT and @@token_cache == token_hash + + # Check sessions + @@session_cache = Store.hgetall(SESSIONS_KEY) + @@session_cache_time = time + return true if @@session_cache[token] + # Check Direct password @@token_cache = Store.get(PRIMARY_KEY) - @@token_cache_time = Time.now + @@token_cache_time = time return true if @@token_cache == token_hash # 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 @@ -54,6 +70,7 @@ def self.verify(token) def self.set(token, old_token, key = PRIMARY_KEY) raise "token must not be nil or empty" if token.nil? or token.empty? + raise "token must be at least 8 characters" if token.length < MIN_TOKEN_LENGTH if set?(key) raise "old_token must not be nil or empty" if old_token.nil? or old_token.empty? @@ -62,6 +79,18 @@ def self.set(token, old_token, key = PRIMARY_KEY) Store.set(key, hash(token)) end + def self.generate_session + token = SecureRandom.urlsafe_base64(nil, false) + Store.hset(SESSIONS_KEY, token, Time.now.iso8601) + return token + end + + def self.logout + Store.del(SESSIONS_KEY) + @@sessions_cache = nil + @@sessions_cache_time = nil + end + def self.hash(token) Digest::SHA2.hexdigest token end