diff --git a/shard.yml b/shard.yml index 36d941c..526ddee 100644 --- a/shard.yml +++ b/shard.yml @@ -9,9 +9,9 @@ authors: targets: authd: - main: src/main.cr + main: src/server.cr authc: - main: utils/authc.cr + main: src/client.cr crystal: 1.7.1 diff --git a/src/authd.cr b/src/authd.cr index 46d2ee9..23255da 100644 --- a/src/authd.cr +++ b/src/authd.cr @@ -1,239 +1,40 @@ -extend AuthD +require "uuid" +require "option_parser" +require "openssl" +require "colorize" +require "jwt" +require "grok" +require "dodb" +require "ipc" + +require "baguette-crystal-base" + +# Allows get configuration from a provided file. +# See Baguette::Configuration::Base.get class Baguette::Configuration class Auth < IPC - property recreate_indexes : Bool = false - property storage : String = "storage" - property registrations : Bool = false - property require_email : Bool = false - property activation_template : String = "email-activation" - property recovery_template : String = "email-recovery" - property mailer_exe : String = "mailer" - property read_only_profile_keys : Array(String) = Array(String).new + include YAML::Serializable - property print_password_recovery_parameters : Bool = false + property login : String? = nil + property pass : String? = nil + property shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes: + property shared_key_file : String? = nil + + def initialize + end end end -# Provides a JWT-based authentication scheme for service-specific users. -class AuthD::Service < IPC - property configuration : Baguette::Configuration::Auth +# Token and user classes. +require "./authd/token.cr" +require "./authd/user.cr" - # DB and its indexes. - property users : DODB::DataBase(User) - property users_per_uid : DODB::Index(User) - property users_per_login : DODB::Index(User) +# Requests and responses. +require "./authd/exceptions" - # #{@configuration.storage}/last_used_uid - property last_uid_file : String +# Requests and responses. +require "./network" - def initialize(@configuration) - super() - - @users = DODB::DataBase(User).new @configuration.storage - @users_per_uid = @users.new_index "uid", &.uid.to_s - @users_per_login = @users.new_index "login", &.login - - @last_uid_file = "#{@configuration.storage}/last_used_uid" - - if @configuration.recreate_indexes - @users.reindex_everything! - end - - self.timer @configuration.ipc_timer - self.service_init "auth" - end - - def hash_password(password : String) : String - digest = OpenSSL::Digest.new "sha256" - digest << password - digest.hexfinal - end - - # new_uid reads the last given UID and returns it incremented. - # Splitting the retrieval and record of new user ids allows to - # only increment when an user fully registers, thus avoiding a - # Denial of Service attack. - # - # WARNING: to record this new UID, new_uid_commit must be called. - # WARNING: new_uid isn't thread safe. - def new_uid - begin - uid = File.read(@last_uid_file).to_i - rescue - uid = 999 - end - - uid += 1 - end - - # new_uid_commit records the new UID. - # WARNING: new_uid_commit isn't thread safe. - def new_uid_commit(uid : Int) - File.write @last_uid_file, uid.to_s - end - - def handle_request(event : IPC::Event) - request_start = Time.utc - - array = event.message.not_nil! - slice = Slice.new array.to_unsafe, array.size - message = IPCMessage::TypedMessage.deserialize slice - request = AuthD.requests.parse_ipc_json message.not_nil! - - if request.nil? - raise "unknown request type" - end - - request_name = request.class.name.sub /^AuthD::Request::/, "" - Baguette::Log.debug "<< #{request_name}" - - response = begin - request.handle self - rescue e : UserNotFound - Baguette::Log.error "#{request_name} user not found" - AuthD::Response::Error.new "authorization error" - rescue e : AuthenticationInfoLacking - Baguette::Log.error "#{request_name} lacking authentication info" - AuthD::Response::Error.new "authorization error" - rescue e : AdminAuthorizationException - Baguette::Log.error "#{request_name} admin authentication failed" - AuthD::Response::Error.new "authorization error" - rescue e - Baguette::Log.error "#{request_name} generic error #{e}" - AuthD::Response::Error.new "unknown error" - end - - # If clients sent requests with an “id” field, it is copied - # in the responses. Allows identifying responses easily. - response.id = request.id - - schedule event.fd, response - - duration = Time.utc - request_start - - response_name = response.class.name.sub /^AuthD::Response::/, "" - - if response.is_a? AuthD::Response::Error - Baguette::Log.warning ">> #{response_name} (#{response.reason})" - else - Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" - end - end - - def get_user_from_token(token : String) - token_payload = Token.from_s(@configuration.shared_key, token) - - @users_per_uid.get? token_payload.uid.to_s - end - - def run - Baguette::Log.title "Starting authd" - - Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}" - Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}" - - self.loop do |event| - case event.type - when LibIPC::EventType::Timer - Baguette::Log.debug "Timer" if @configuration.print_ipc_timer - - when LibIPC::EventType::MessageRx - Baguette::Log.debug "Received message from #{event.fd}" if @configuration.print_ipc_message_received - begin - handle_request event - rescue e - Baguette::Log.error "#{e.message}" - # send event.fd, Response::Error.new e.message - end - - when LibIPC::EventType::MessageTx - Baguette::Log.debug "Message sent to #{event.fd}" if @configuration.print_ipc_message_sent - - when LibIPC::EventType::Connection - Baguette::Log.debug "Connection from #{event.fd}" if @configuration.print_ipc_connection - when LibIPC::EventType::Disconnection - Baguette::Log.debug "Disconnection from #{event.fd}" if @configuration.print_ipc_disconnection - else - Baguette::Log.error "Not implemented behavior for event: #{event}" - end - end - - end -end - - -begin - simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser - - configuration = if no_configuration - Baguette::Log.info "do not load a configuration file." - Baguette::Configuration::Auth.new - else - Baguette::Configuration::Auth.get(configuration_file) || - Baguette::Configuration::Auth.new - end - - Baguette::Context.verbosity = configuration.verbosity - - if key_file = configuration.shared_key_file - configuration.shared_key = File.read(key_file).chomp - end - - OptionParser.parse do |parser| - parser.banner = "usage: authd [options]" - - parser.on "--storage directory", "Directory in which to store users." do |directory| - configuration.storage = directory - end - - parser.on "-K file", "--key-file file", "JWT key file" do |file_name| - configuration.shared_key = File.read(file_name).chomp - end - - parser.on "-R", "--allow-registrations", "Allow user registration." do - configuration.registrations = true - end - - parser.on "-E", "--require-email", "Require an email." do - configuration.require_email = true - end - - parser.on "-t activation-template-name", "--activation-template name", "Email activation template." do |opt| - configuration.activation_template = opt - end - - parser.on "-r recovery-template-name", "--recovery-template name", "Email recovery template." do |opt| - configuration.recovery_template = opt - end - - parser.on "-m mailer-exe", "--mailer mailer-exe", "Application to send registration emails." do |opt| - configuration.mailer_exe = opt - end - - - parser.on "-x key", "--read-only-profile-key key", "Marks a user profile key as being read-only." do |key| - configuration.read_only_profile_keys.push key - end - - parser.on "-h", "--help", "Show this help" do - puts parser - exit 0 - end - end - - if simulation - pp! configuration - exit 0 - end - - AuthD::Service.new(configuration).run - -rescue e : OptionParser::Exception - Baguette::Log.error e.message -rescue e - Baguette::Log.error "exception raised: #{e.message}" - e.backtrace.try &.each do |line| - STDERR << " - " << line << '\n' - end -end +# Functions to request the authd server. +require "./authd/client.cr" diff --git a/utils/better-parser.cr b/src/better-parser.cr similarity index 100% rename from utils/better-parser.cr rename to src/better-parser.cr diff --git a/utils/authc.cr b/src/client.cr similarity index 98% rename from utils/authc.cr rename to src/client.cr index a212827..4511ec0 100644 --- a/utils/authc.cr +++ b/src/client.cr @@ -1,11 +1,6 @@ require "option_parser" - -require "ipc" require "yaml" - -require "baguette-crystal-base" - -require "../src/libauth.cr" +require "./authd.cr" class Context class_property simulation = false # do not perform the action diff --git a/src/libauth.cr b/src/libauth.cr deleted file mode 100644 index 181dc3d..0000000 --- a/src/libauth.cr +++ /dev/null @@ -1,34 +0,0 @@ -require "json" -require "jwt" -require "ipc" - -require "baguette-crystal-base" - -# Allows get configuration from a provided file. -# See Baguette::Configuration::Base.get -class Baguette::Configuration - class Auth < IPC - include YAML::Serializable - - property login : String? = nil - property pass : String? = nil - property shared_key : String = "nico-nico-nii" # Default authd key, as per the specs. :eyes: - property shared_key_file : String? = nil - - def initialize - end - end -end - -# Token and user classes. -require "./authd/token.cr" -require "./authd/user.cr" - -# Requests and responses. -require "./authd/exceptions" - -# Requests and responses. -require "./network" - -# Functions to request the authd server. -require "./authd/client.cr" diff --git a/src/main.cr b/src/main.cr deleted file mode 100644 index a2d7941..0000000 --- a/src/main.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "uuid" -require "option_parser" -require "openssl" -require "colorize" -require "jwt" -require "grok" - -require "dodb" -require "baguette-crystal-base" - -require "ipc" -require "./libauth.cr" -require "./authd.cr" diff --git a/src/server.cr b/src/server.cr new file mode 100644 index 0000000..0594303 --- /dev/null +++ b/src/server.cr @@ -0,0 +1,241 @@ +require "./authd.cr" + +extend AuthD + +class Baguette::Configuration + class Auth < IPC + property recreate_indexes : Bool = false + property storage : String = "storage" + property registrations : Bool = false + property require_email : Bool = false + property activation_template : String = "email-activation" + property recovery_template : String = "email-recovery" + property mailer_exe : String = "mailer" + property read_only_profile_keys : Array(String) = Array(String).new + + property print_password_recovery_parameters : Bool = false + end +end + +# Provides a JWT-based authentication scheme for service-specific users. +class AuthD::Service < IPC + property configuration : Baguette::Configuration::Auth + + # DB and its indexes. + property users : DODB::DataBase(User) + property users_per_uid : DODB::Index(User) + property users_per_login : DODB::Index(User) + + # #{@configuration.storage}/last_used_uid + property last_uid_file : String + + def initialize(@configuration) + super() + + @users = DODB::DataBase(User).new @configuration.storage + @users_per_uid = @users.new_index "uid", &.uid.to_s + @users_per_login = @users.new_index "login", &.login + + @last_uid_file = "#{@configuration.storage}/last_used_uid" + + if @configuration.recreate_indexes + @users.reindex_everything! + end + + self.timer @configuration.ipc_timer + self.service_init "auth" + end + + def hash_password(password : String) : String + digest = OpenSSL::Digest.new "sha256" + digest << password + digest.hexfinal + end + + # new_uid reads the last given UID and returns it incremented. + # Splitting the retrieval and record of new user ids allows to + # only increment when an user fully registers, thus avoiding a + # Denial of Service attack. + # + # WARNING: to record this new UID, new_uid_commit must be called. + # WARNING: new_uid isn't thread safe. + def new_uid + begin + uid = File.read(@last_uid_file).to_i + rescue + uid = 999 + end + + uid += 1 + end + + # new_uid_commit records the new UID. + # WARNING: new_uid_commit isn't thread safe. + def new_uid_commit(uid : Int) + File.write @last_uid_file, uid.to_s + end + + def handle_request(event : IPC::Event) + request_start = Time.utc + + array = event.message.not_nil! + slice = Slice.new array.to_unsafe, array.size + message = IPCMessage::TypedMessage.deserialize slice + request = AuthD.requests.parse_ipc_json message.not_nil! + + if request.nil? + raise "unknown request type" + end + + request_name = request.class.name.sub /^AuthD::Request::/, "" + Baguette::Log.debug "<< #{request_name}" + + response = begin + request.handle self + rescue e : UserNotFound + Baguette::Log.error "#{request_name} user not found" + AuthD::Response::Error.new "authorization error" + rescue e : AuthenticationInfoLacking + Baguette::Log.error "#{request_name} lacking authentication info" + AuthD::Response::Error.new "authorization error" + rescue e : AdminAuthorizationException + Baguette::Log.error "#{request_name} admin authentication failed" + AuthD::Response::Error.new "authorization error" + rescue e + Baguette::Log.error "#{request_name} generic error #{e}" + AuthD::Response::Error.new "unknown error" + end + + # If clients sent requests with an “id” field, it is copied + # in the responses. Allows identifying responses easily. + response.id = request.id + + schedule event.fd, response + + duration = Time.utc - request_start + + response_name = response.class.name.sub /^AuthD::Response::/, "" + + if response.is_a? AuthD::Response::Error + Baguette::Log.warning ">> #{response_name} (#{response.reason})" + else + Baguette::Log.debug ">> #{response_name} (Total duration: #{duration})" + end + end + + def get_user_from_token(token : String) + token_payload = Token.from_s(@configuration.shared_key, token) + + @users_per_uid.get? token_payload.uid.to_s + end + + def run + Baguette::Log.title "Starting authd" + + Baguette::Log.info "(mailer) Email activation template: #{@configuration.activation_template}" + Baguette::Log.info "(mailer) Email recovery template: #{@configuration.recovery_template}" + + self.loop do |event| + case event.type + when LibIPC::EventType::Timer + Baguette::Log.debug "Timer" if @configuration.print_ipc_timer + + when LibIPC::EventType::MessageRx + Baguette::Log.debug "Received message from #{event.fd}" if @configuration.print_ipc_message_received + begin + handle_request event + rescue e + Baguette::Log.error "#{e.message}" + # send event.fd, Response::Error.new e.message + end + + when LibIPC::EventType::MessageTx + Baguette::Log.debug "Message sent to #{event.fd}" if @configuration.print_ipc_message_sent + + when LibIPC::EventType::Connection + Baguette::Log.debug "Connection from #{event.fd}" if @configuration.print_ipc_connection + when LibIPC::EventType::Disconnection + Baguette::Log.debug "Disconnection from #{event.fd}" if @configuration.print_ipc_disconnection + else + Baguette::Log.error "Not implemented behavior for event: #{event}" + end + end + + end +end + + +begin + simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser + + configuration = if no_configuration + Baguette::Log.info "do not load a configuration file." + Baguette::Configuration::Auth.new + else + Baguette::Configuration::Auth.get(configuration_file) || + Baguette::Configuration::Auth.new + end + + Baguette::Context.verbosity = configuration.verbosity + + if key_file = configuration.shared_key_file + configuration.shared_key = File.read(key_file).chomp + end + + OptionParser.parse do |parser| + parser.banner = "usage: authd [options]" + + parser.on "--storage directory", "Directory in which to store users." do |directory| + configuration.storage = directory + end + + parser.on "-K file", "--key-file file", "JWT key file" do |file_name| + configuration.shared_key = File.read(file_name).chomp + end + + parser.on "-R", "--allow-registrations", "Allow user registration." do + configuration.registrations = true + end + + parser.on "-E", "--require-email", "Require an email." do + configuration.require_email = true + end + + parser.on "-t activation-template-name", "--activation-template name", "Email activation template." do |opt| + configuration.activation_template = opt + end + + parser.on "-r recovery-template-name", "--recovery-template name", "Email recovery template." do |opt| + configuration.recovery_template = opt + end + + parser.on "-m mailer-exe", "--mailer mailer-exe", "Application to send registration emails." do |opt| + configuration.mailer_exe = opt + end + + + parser.on "-x key", "--read-only-profile-key key", "Marks a user profile key as being read-only." do |key| + configuration.read_only_profile_keys.push key + end + + parser.on "-h", "--help", "Show this help" do + puts parser + exit 0 + end + end + + if simulation + pp! configuration + exit 0 + end + + AuthD::Service.new(configuration).run + +rescue e : OptionParser::Exception + Baguette::Log.error e.message +rescue e + Baguette::Log.error "exception raised: #{e.message}" + e.backtrace.try &.each do |line| + STDERR << " - " << line << '\n' + end +end