Class: Pixiurge::AuthenticatedApp
- Defined in:
- lib/pixiurge/authentication.rb
Overview
Pixiurge::AuthenticatedApp is a parent class for applications (games) that want the built-in Pixiurge accounts and authentication.
AuthenticatedApp only allows a single login by each player at one time. That's not a hard limit of Pixiurge, but AuthenticatedApp requires it. Similarly, AuthenticatedApp adds the requirement that you register an account for each player before logging in.
For more details of how to implement on top of it, see AppInterface.
Implementation
It's not terribly hard to roll your own authentication system in Ruby. It is easy to make something horrifically insecure any time you're doing browser/server communication. Pixiurge makes this neither harder nor easier than usual. The built-in authentication is simple and pretty okay if you keep the requirement for HTTPS/WSS for messages. If you use this over an unencrypted connection, all your password-equivalents can be stolen for your game, but it shouldn't leak the actual passwords -- passwords are never sent to the server, though they are preserved unencrypted in browser cookies.
There are lots of potential ways to implements accounts and authentication. If you care a lot about it, may I recommend rolling your own? At a minimum, it's very easy to need better account storage than the default built-in version.
The built-in authentication and accounts class makes several simplifying assumptions. Accounts are held in a JSON file. Client-side BCrypt is used to slow attackers trying to brute-force a password, and to keep rainbow tables from helping significantly. We assume all exchange of messages is done over HTTPS and/or WSS, which means that TLS/SSL encryption keeps the client-side BCrypted key from acting as a simple reusable token.
This works fine for now, but is intentionally easy to replace or modify if needed later. It would be reasonable to store passwords in a database, for instance. One caveat: passwords should not be stored in the Engine data, because we don't want various types of rollbacks to affect them, nor do we want in-game administrators to easily query them. The latter is hard to ensure, but it's still better not to put them directly in the in-engine data.
Constant Summary
- USERNAME_REGEX =
Regex for what usernames are allowed
/\A[a-zA-Z0-9]+\Z/
- INIT_OPTIONS =
Legal options to pass to Pixiurge::AuthenticatedApp#new
Pixiurge::App::INIT_OPTIONS + [ :accounts_file, :storage ]
Constants inherited from App
Instance Attribute Summary
Attributes inherited from App
#debug, #incoming_traffic_logfile, #outgoing_traffic_logfile, #record_traffic
Instance Method Summary collapse
-
#all_logged_in_usernames ⇒ Array<String>
Return a list of all currently connected usernames.
-
#handle_message(websocket, args) ⇒ void
Process an authorization message or fall back to old message handling.
-
#initialize(options = { :debug => false, :record_traffic => false, :incoming_traffic_logfile => "log/incoming_traffic.json", :outgoing_traffic_logfile => "log/outgoing_traffic.json", :accounts_file => "accounts.json", :storage => nil }) ⇒ void
constructor
Constructor.
-
#on_close(ws, code, reason) ⇒ void
This handler will be called when a websocket connection is closed.
-
#on_login(ws, username) ⇒ void
If you inherit from AuthenticatedApp, this handler will be called when a player has successfully logged in.
-
#on_message(ws, data) ⇒ void
If you inherit from AuthenticatedApp, this handler will be called when a websocket receives a message of a miscellaneous type -- not authentication or a player action, for instance.
-
#username_for_websocket(websocket) ⇒ String?
For a given websocket object, get its associated username.
-
#websocket_for_username(websocket) ⇒ Websocket?
For a given account username, get its associated websocket if any.
Methods inherited from App
#coffeescript_dirs, #handler, #on_event, #rack_builder, #root_dir, #root_redirect, #static_dirs, #static_files, #tilt_dirs, #tmx_dirs, #websocket_handler
Constructor Details
#initialize(options = { :debug => false, :record_traffic => false, :incoming_traffic_logfile => "log/incoming_traffic.json", :outgoing_traffic_logfile => "log/outgoing_traffic.json", :accounts_file => "accounts.json", :storage => nil }) ⇒ void
Constructor
67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/pixiurge/authentication.rb', line 67 def initialize( = { :debug => false, :record_traffic => false, :incoming_traffic_logfile => "log/incoming_traffic.json", :outgoing_traffic_logfile => "log/outgoing_traffic.json", :accounts_file => "accounts.json", :storage => nil }) = .keys - INIT_OPTIONS raise("Illegal options passed to AuthenticatedApp.new: #{.inspect}!") unless .empty? sub_opt = {} # TODO: use Hash#slice when I'm ready to restrict to higher Ruby versions ::Pixiurge::App::INIT_OPTIONS.each { |opt| sub_opt[opt] = [opt] } super sub_opt @storage = [:storage] || Pixiurge::Authentication::FileAccountStorage.new([:accounts_file] || "accounts.json") @username_for_websocket = {} @websocket_for_username = {} nil end |
Instance Method Details
#all_logged_in_usernames ⇒ Array<String>
Return a list of all currently connected usernames.
224 225 226 |
# File 'lib/pixiurge/authentication.rb', line 224 def all_logged_in_usernames @username_for_websocket.keys end |
#handle_message(websocket, args) ⇒ void
This method returns an undefined value.
Process an authorization message or fall back to old message handling.
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 136 137 138 139 140 |
# File 'lib/pixiurge/authentication.rb', line 89 def (websocket, args) msg_type = args[0] if msg_type == Pixiurge::Protocol::Incoming::AUTH_REGISTER_ACCOUNT username, salt, hashed = args[1]["username"], args[1]["salt"], args[1]["bcrypted"] user_state = @storage.data_for(username) if user_state return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_FAILED_REGISTRATION, { "message" => "Account #{username.inspect} already exists!" } end unless username =~ USERNAME_REGEX return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_FAILED_REGISTRATION, { "message" => "Username contains illegal characters: #{username.inspect}!" } end @storage.register(username, { "account" => { "salt" => salt, "hashed" => hashed, "method" => "bcrypt" } }) websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_REGISTRATION, { "username" => username } return end if msg_type == Pixiurge::Protocol::Incoming::AUTH_LOGIN username, hashed = args[1]["username"], args[1]["bcrypted"] user_state = @storage.data_for(username) unless user_state return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_FAILED_LOGIN, { "message" => "No such user as #{username.inspect}!" } end if user_state["account"]["hashed"] == hashed # Let the browser side know that a login succeeded websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_LOGIN, { "username" => username } # Let the app know that a login succeeded return send_event "login", websocket, username else return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_FAILED_LOGIN, { "message" => "Wrong password for user #{username.inspect}!" } end # Unreachable, already returned end if msg_type == Pixiurge::Protocol::Incoming::AUTH_GET_SALT username = args[1]["username"] user_state = @storage.data_for(username) unless user_state return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_FAILED_LOGIN, { "message" => "No such user as #{username.inspect}!" } end user_salt = user_state["account"]["salt"] return websocket_send websocket, Pixiurge::Protocol::Outgoing::AUTH_SALT, { "salt" => user_salt } end if msg_type == Pixiurge::Protocol::Incoming::PLAYER_ACTION username = @username_for_websocket[websocket] raise("No player actions allowed from non-logged-in websockets!") unless username return send_event "player_action", username, args[1..-1] end super end |
#on_close(ws, code, reason) ⇒ void
This method returns an undefined value.
This handler will be called when a websocket connection is closed. It can be used for cleaning up player-related data structures.
193 194 195 196 197 198 199 200 |
# File 'lib/pixiurge/authentication.rb', line 193 def on_close(ws, code, reason) username = @username_for_websocket.delete(ws) if username && @websocket_for_username[username] == ws @websocket_for_username.delete(username) send_event "player_logout", username end nil end |
#on_login(ws, username) ⇒ void
This method returns an undefined value.
If you inherit from AuthenticatedApp, this handler will be called when a player has successfully logged in. Later handlers will continue passing the websocket object. This handler lets you associate the websocket with a specific player account. If you override this method, make sure to call super() so that the AuthenticatedApp code can implement methods like #username_for_websocket.
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/pixiurge/authentication.rb', line 154 def on_login(ws, username) reconnect = false # This websocket is already logged in. Weird. Close the new connection. if @username_for_websocket.has_key?(ws) websocket_send ws, Pixiurge::Protocol::Outgoing::AUTH_FAILED_LOGIN, { "message" => "Your websocket is somehow already logged in! Failing!" } ws.close(1000, "Websocket already logged in, failing") # @todo Figure out websocket error codes well enough to know what to return here return end # This username is already logged in, so we'll override. Close the old connection. if @websocket_for_username.has_key?(username) old_ws = @websocket_for_username[username] websocket_send old_ws, Pixiurge::Protocol::Outgoing::DISCONNECTION, { "message" => "You have just logged in from a new location - disconnecting your old location." } old_ws.close(1000, "You have been disconnected in favor of a new connection by your account.") @username_for_websocket.delete(old_ws) reconnect = true end @username_for_websocket[ws] = username @websocket_for_username[username] = ws if reconnect send_event "player_reconnect", username else send_event "player_login", username end nil end |
#on_message(ws, data) ⇒ void
This method returns an undefined value.
If you inherit from AuthenticatedApp, this handler will be called when a websocket receives a message of a miscellaneous type -- not authentication or a player action, for instance.
236 237 238 239 240 241 |
# File 'lib/pixiurge/authentication.rb', line 236 def (ws, data) username = @username_for_websocket[ws] if username && data.is_a?(Array) && data[0] == Pixiurge::Protocol::Incoming::PLAYER_ACTION send_event "player_action", username, *data end end |
#username_for_websocket(websocket) ⇒ String?
For a given websocket object, get its associated username.
207 208 209 |
# File 'lib/pixiurge/authentication.rb', line 207 def username_for_websocket(websocket) @username_for_websocket[websocket] end |
#websocket_for_username(websocket) ⇒ Websocket?
For a given account username, get its associated websocket if any.
216 217 218 |
# File 'lib/pixiurge/authentication.rb', line 216 def websocket_for_username(websocket) @websocket_for_username[websocket] end |