Class: Pixiurge::AuthenticatedApp

Inherits:
App
  • Object
show all
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.

See Also:

Since:

  • 0.1.0

Constant Summary

USERNAME_REGEX =

Regex for what usernames are allowed

Since:

  • 0.1.0

/\A[a-zA-Z0-9]+\Z/
INIT_OPTIONS =

Legal options to pass to Pixiurge::AuthenticatedApp#new

Since:

  • 0.1.0

Pixiurge::App::INIT_OPTIONS + [ :accounts_file, :storage ]

Constants inherited from App

Pixiurge::App::EVENTS

Instance Attribute Summary

Attributes inherited from App

#debug, #incoming_traffic_logfile, #outgoing_traffic_logfile, #record_traffic

Instance Method Summary collapse

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

Parameters:

  • options (Hash) (defaults to: { :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 })

    Options to configure app behavior

Options Hash (options):

  • :accounts_file (String)

    JSON file to hold the accounts. Defaults to "accounts.json".

  • :storage (Pixiurge::Authentication::AccountStorage)

    An object to hold the accounts matching the AccountStorage interface

  • :debug (Boolean)

    Whether to print debug output

  • :record_traffic (Boolean)

    Whether to record incoming and outgoing websocket traffic to logfiles

  • :incoming_traffic_logfile (String)

    Pathname to record incoming websocket traffic

  • :outgoing_traffic_logfile (String)

    Pathname to record outgoing websocket traffic

Since:

  • 0.1.0



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/pixiurge/authentication.rb', line 67

def 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 })
  illegal_options = options.keys - INIT_OPTIONS
  raise("Illegal options passed to AuthenticatedApp.new: #{illegal_options.inspect}!") unless illegal_options.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] = options[opt] }
  super sub_opt
  @storage = options[:storage] || Pixiurge::Authentication::FileAccountStorage.new(options[:accounts_file] || "accounts.json")
  @username_for_websocket = {}
  @websocket_for_username = {}
  nil
end

Instance Method Details

#all_logged_in_usernamesArray<String>

Return a list of all currently connected usernames.

Returns:

  • (Array<String>)

    Usernames for all connected accounts.

Since:

  • 0.1.0



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.

Parameters:

  • websocket (Websocket)

    A websocket object, as defined by the websocket-driver gem

  • args (Array)

    Additional message-type-specific arguments

See Also:

Since:

  • 0.1.0



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 handle_message(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.

Parameters:

  • ws (#on)

    A Websocket-Driver websocket object

  • code (Integer)

    A Websocket protocol onclose status code (see https://tools.ietf.org/html/rfc6455)

  • reason (String)

    A reason for the websocket closing

Since:

  • 0.1.0



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.

Parameters:

  • ws (Websocket)

    A Websocket-Driver websocket object

  • username (String)

    The username for the account

Since:

  • 0.1.0



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 (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.

Parameters:

  • ws (Websocket)

    A Websocket-Driver websocket object

  • data (Object)

    Deserialized JSON message data

Since:

  • 0.1.0



236
237
238
239
240
241
# File 'lib/pixiurge/authentication.rb', line 236

def on_message(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.

Parameters:

  • websocket (Websocket)

    The Websocket object to query

Returns:

  • (String, nil)

    The username for this websocket or nil if there is none currently

Since:

  • 0.1.0



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.

Parameters:

  • websocket (String)

    The account username to query

Returns:

  • (Websocket, nil)

    The websocket for this account if it's logged in, or nil if not

Since:

  • 0.1.0



216
217
218
# File 'lib/pixiurge/authentication.rb', line 216

def websocket_for_username(websocket)
  @websocket_for_username[websocket]
end