Closed scally closed 6 years ago
The framework needs to assume something about players in order to allow working with them. I didn't find anything in multiplayer.md, so I'd imagine we could add a parapraph about this.
There is currently no notion of an game instance id that comes from the server, associating players to a specific running game. Instead, the client is assumed to provide a gameID
which fulfills this purpose. There is a paragraph about that in the aforementioned docs. In true non-demo, the client currently is assumed to generate a unique gameID
for each separate game instance.
Take this with a grain of salt, I didn't experiment with the code to get this knowledge, I've only read it.
I'll make the documentation clearer about this, but all the playerID/gameID management is currently the responsibility of the user. I'd like to eventually ship a lobby implementation that will do all this for you, but we aren't there yet.
playerID's are '0', '1', etc. as you rightly point out, and deliberately so. Figuring out how to map your notion of a user to these ID's is entirely up to you.
For example, you might have your own user authentication system where the user is identified by something like "Bob" or "Alice". You need to build some sort of game creation interface where users are assigned to specific seats ("0", "1", "2" etc.) in a game. This ID can then be stored in a cookie or whatever mechanism you choose and passed via the playerID
prop.
You might opt for something simpler where the playerID
/gameID
is determined by the URL, and you distribute different URL's to different players. This is a more manual setup, but you don't need a user login system at all.
@nicolodavis Your response makes sense, but what I'm still a bit lost on is how else to secure the playerID. If it's always deterministic, isn't it pretty easily spoofable?
That is, you wouldn't send a session ID to a server with an ID of "1", because it would be super easy to guess the other player IDs and pretend to be them. Without some way of the server identifying you by session, how would you prevent similar tampering?
Yes, it is spoofable at the moment.
We need a few things probably:
Some sort of "create game" logic that creates a game and secrets associated with each player.
A "join game" call that sends over secrets to a client for a particular player.
The client then sends over the secret instead of the playerID to play.
I think we should still support non authenticated games like today for people that don't want any of this security. This can be a simple Boolean in the "create game" call.
@nicolodavis thanks for hearing me out. 😄 I'll think on this a bit and see if I can sketch something towards it this week.
So, I've thought on this a bit.
If the "create game" bits create player secrets, that could work for encryption of one game session.
What I'm then wondering about is if you wanted some kind of persistence of the concept of a player -- to track wins, or leaderboards, or scores -- some kind of persistent state between games. If the game creation steps create a player secret, and it's tied to that individual session, then there's not really a way for someone to be permanently tied to any kind of persisting identity. You could make that player secret something the client holds onto between games, at least, although there's no way to ever switch devices/browsers.
For some multiplayer games, anonymous matchmaking with no persistence might be enough, but there's going to be a lot of use cases where you'd want to let a user's actions carry over past the end of a game session.
The conclusion I come to after these thoughts is that you'd want something sort of like oauth on the server side -- the client would auth through some kind of identity provider and the server would be responsible for reading the secret and then tying that identity to some persistent server-side concept.
That way, the server doesn't have to have a login/password store that would have to be secured, it would just delegate auth to somewhere else, and just care that it has a token to prove you're user XYZ.
Implementation would look like some new actions for logging in, and then an area within the Game object to store Sessions/Lobbies/Leaderboards, with data stripped of anything not pertaining to the current session for the player on the client side, similar to the existing secret system boardgame.io already supports. Action payloads would have to contain the token, which the server would check with the auth provider.
You would still need some kind of zero-config option here for anonymous sessions to preserve the simplicity for people who don't need authentication for players.
@nicolodavis can you think of an easier way to have persistent user identity in this system besides something similar to what I'm describing?
@scally My post above describes how to prevent spoofing, i.e. clients that pretend that they're part of a particular game and execute moves.
What you're describing is a reasonable implementation for a lobby / leaderboard. boardgame.io is designed so that you can integrate it into your own user data without imposing a structure to that data. You can implement your own user authentication system (or use oauth) and then store all the user related data in your own database. boardgame.io just takes over when a game is actually played.
For example, you can use your own auth scheme to determine that a player called Alice has logged in to your website. boardgame.io doesn't even need to know about Alice. Your website implementation then does the following (as part of your lobby implementation):
gameID: "game-0",
numPlayers: 2,
players: {
"0": { secret: "secret-0" },
"1": { secret: "secret-1" },
}
gameID: "game-0",
numPlayers: 2,
players: {
"0": { secret: "secret-0", name: "Alice" },
"1": { secret: "secret-1", name: "Bob" },
}
The names are just for reference (so we can display them). They're not used for authentication. The framework will then accept moves for Player 0 if the player supplies "secret-0" during play (only Alice can do this).
We can of course provide a bundled lobby implementation that does all of this for users that want true zero config (I would be really excited if someone wants to work on this), but we should make sure that the underlying API is de-coupled enough from it so that others can integrate it with their existing lobby implementations easily.
@nicolodavis Is it fair to say then that there's two issues/PR's here? One for game join/creation logic and another for lobbying/persistence?
Hmm, I might be misunderstanding this separation a bit. Let me look over your answer some more.
Yes, there are two components which would fit naturally into two PRs.
I think the lobby that we could bundle in should be fairly straightforward, though. No real persistence. Just have the user fill in their username (no password) and have some UI to create games and allow others to join. This will create a good base for people that want to add OAuth or something else on top of that.
@nicolodavis I have a super sketchy PR for this to see if this is sort of on the right track for what you were thinking. It creates routes for the two named API actions on a separate Koa app, and then has some non-working code that documents the approach. Please let me know if this is the right direction or not.
Some thoughts after looking through your PR (sorry about the delay in getting to this):
There are going to be two distinct "databases" (in reality they will most likely be two tables within one database, but I just want to make the conceptual difference stand out).
This holds the game data. This is what we already have today. We even have an API (db.set
and db.get
) that allows any database backend to be connected in order to persist games. There is one entry in this database for every game that is currently in progress. Since we don't currently have a create game
API at the moment, the server just creates a game on demand during the sync here. This is exactly what we need to pull out and facilitate with a proper lobby API instead. In other words, your post handler should have a db.set
that adds an entry to this database on create game
.
This contains lobby metadata. This is what we would use to hold current open invitations to games, for example. It could also hold user data if we want to authenticate users too.
I'll add more comments directly on the PR.
@nicolodavis Is your expectation right now that "joining" a game is just adding a named label to an ordinal player? Does it have some other meaning?
That and distributing the secret to the player as well.
@nicolodavis If the secret is generated on the lobby system (elsewhere, outside), wouldn't that system - that also authenticated the player, be responsible for giving it to them?
The lobby API we're building (create game and join game) should do everything we expect as far as these operations are concerned. I'm hoping that a lobby implementation will just use these two calls under the hood. Which means that if we don't do all the secret generation here, we're pushing the burden onto the lobby implementation, which should only be concerned with logging in users and such. Note that the secrets we're talking about are different. If the lobby implementation is going to do some user authentication, it will use some secret (a password) to allow the user to login. The secret I'm referring to is generated on demand when a user joins a game. This is separate from the secret used to login. Think of it more like a token that enables the user to play. In fact, we could allow for it to be invalidated in the future (to support kicking players out of games)., so it would be good to keep it decoupled from the secret used to log in the user.
@nicolodavis summarizing my understanding:
create_game -> creates a game & a boardgame.io player secret for each player in the game. join_game -> associates the player to a name, returns the boardgame.io player secret in its response
Is this accurate?
Yep!
@nicolodavis Still working on this. Were you thinking the secrets would be stored to db2
in your example?
Yeah, that sounds good. Right now, this is what our DB looks like:
gameid-1: {
G: ...
ctx: ...
}
gameid-2: {
G: ...
ctx: ...
}
...
We could add the game metadata in a parallel key space like this:
gameid-1: {
players: {
'0': { secret: '' }
...
}
}
gameid-2: {
players: {
'0': { secret: '' }
...
}
}
...
One way to achieve the separation of the key space would be to do something like this:
db.set('gameid', gameState);
db.set('gameid:metadata', metadata);
We could in theory also change our DB API to support the concept of a table, so we can do:
db.set({
table: 'metadata',
key: 'gameid',
value: metadata
});
and depending on whether the implementation under the hood supports tables or not, it could either use our key augmentation trick or actually write to a separate table if that's a possibility.
@nicolodavis thanks. I used the gameid:metadata
version for now, we can upgrade to something more like the table-based version later perhaps? I updated my PR, could you please take a look and see if you feel that it's going in a good direction? 😄
I'll close this now that we have whatever we discussed on this thread in. I'll open another issue to track future work.
I've been trying to debug an issue with a multiplayer game this weekend, and encountered the following misunderstanding:
When you provide a playerID to the server today, it must be a 0-indexed number corresponding to the number of players -- [0, 1] for 2 players, for instance, is the only possible space. This appears to be due to this: https://github.com/google/boardgame.io/blob/bb9ff3b6dd6c9f8ea5314d3712d2741b5d8992bc/src/core/flow.js#L563
So, if you provide a playerID outside of the range [0,1], no player can make a move.
I can imagine a few possible solutions:
Otherwise, in a true non-demo multiplayer environment, how would you know which player is 0 and which is 1 without some kind of server-side arbitration?