Skip to content

Commit

Permalink
lobby API support (#189)
Browse files Browse the repository at this point in the history
* Create API support for authenticated games

* package-lock.json

* Change game creation param to `name`

* Test that joining a game that doesnt exist returns a failure

* Add superagent, move supertest to dev dependency

* WIP Setup authenticated game during dev server startup

* Clean up authenticated game setup

* Create API support for authenticated games

* package-lock.json

* Change game creation param to `name`

* Test that joining a game that doesnt exist returns a failure

* Add superagent, move supertest to dev dependency

* WIP Setup authenticated game during dev server startup

* Clean up authenticated game setup

* Throw a 404 when the game to join is not found

* standardize spacing

* add missing headers

* rename api-server.js to api.js

* Move authenticated game setup to authenticated game example

* enable CORS on API server

* disallow changing the gameID in the example

* fix documentation
  • Loading branch information
scally authored and nicolodavis committed May 29, 2018
1 parent 461874b commit 8e2f8c4
Show file tree
Hide file tree
Showing 19 changed files with 1,180 additions and 540 deletions.
42 changes: 42 additions & 0 deletions docs/api/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,45 @@ const server = Server({

server.run(8000);
```

# Authentication

You can optionally choose to require clients to use credential tokens to prove they have the right to send actions on behalf of a player.

Authenticated games are created with server-side tokens for each player. You can create a game with the `games/create` API call, and join a player to a game with the `gameInstances/join` API call.

A game that is authenticated will not accept moves from a client on behalf of a player without the appropriate credential token.

Use the create API call to create a game that requires credential tokens. When you call the join API, you can retrieve the credential token for a particular player.

Authentication APIs are available by default on `WebSocket port` + 1.

### Creating a game

#### `/games/:name/create`

Creates a new authenticated game for a game named `name`.

Accepts one parameter: `numPlayers`, which is required & indicates how many credentials to create.

Returns `gameID`, which is the ID of the newly created game instance.

### Joining a game

#### `/game_instances/:id/join`

Allows a player to join the given game instance `id`.

Accepts three parameters, all required:

`gameName`: the name of the game being joined

`playerID`: the ordinal player in the game that is being joined (0, 1...)

`playerName`: the display name of the player joining the game.

Returns `playerCredentials` which is the token this player will require to authenticate their actions in the future.

### Client Authentication

All actions for an authenticated game require an additional payload field: `credentials`, which must be the given secret associated to the player.
151 changes: 151 additions & 0 deletions examples/react/modules/tic-tac-toe/components/authenticated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright 2018 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

import React from 'react';
import { Client } from 'boardgame.io/react';
import TicTacToe from '../game';
import Board from './board';
import PropTypes from 'prop-types';
import request from 'superagent';

const App = Client({
game: TicTacToe,
board: Board,
debug: false,
multiplayer: true,
});

class AuthenticatedClient extends React.Component {
constructor(props) {
super(props);
this.state = {
gameID: 'gameID',
players: {
'0': {
credentials: 'credentials',
},
'1': {
credentials: 'credentials',
},
},
};
}

async componentDidMount() {
const gameName = 'tic-tac-toe';
const PORT = 8000;

const newGame = await request
.post(`http://localhost:${PORT + 1}/games/${gameName}/create`)
.send({ numPlayers: 2 });

const gameID = newGame.body.gameID;

let playerCredentials = [];

for (let playerID of [0, 1]) {
const player = await request
.patch(`http://localhost:${PORT + 1}/game_instances/${gameID}/join`)
.send({
gameName,
playerID,
playerName: playerID.toString(),
});

playerCredentials.push(player.body.playerCredentials);
}

this.setState({
gameID,
players: {
'0': {
credentials: playerCredentials[0],
},
'1': {
credentials: playerCredentials[1],
},
},
});
}

onPlayerCredentialsChange(playerID, credentials) {
this.setState({
gameID: this.state.gameID,
players: {
...this.state.players,
[playerID]: {
credentials,
},
},
});
}

render() {
return (
<AuthenticatedExample
gameID={this.state.gameID}
players={this.state.players}
onPlayerCredentialsChange={this.onPlayerCredentialsChange.bind(this)}
/>
);
}
}

class AuthenticatedExample extends React.Component {
static propTypes = {
gameID: PropTypes.string,
players: PropTypes.any,
onPlayerCredentialsChange: PropTypes.func,
};

render() {
return (
<div style={{ padding: 50 }}>
<h1>Authenticated</h1>

<p>
Change the credentials of a player, and you will notice that the
server no longer accepts moves from that client.
</p>

<div className="runner">
<div className="run">
<App
gameID={this.props.gameID}
playerID="0"
credentials={this.props.players['0'].credentials}
/>
<input
type="text"
value={this.props.players['0'].credentials}
onChange={event =>
this.props.onPlayerCredentialsChange('0', event.target.value)
}
/>
</div>
<div className="run">
<App
gameID={this.props.gameID}
playerID="1"
credentials={this.props.players['1'].credentials}
/>
<input
type="text"
value={this.props.players['1'].credentials}
onChange={event =>
this.props.onPlayerCredentialsChange('1', event.target.value)
}
/>
</div>
</div>
</div>
);
}
}

export default AuthenticatedClient;
6 changes: 6 additions & 0 deletions examples/react/modules/tic-tac-toe/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Singleplayer from './components/singleplayer';
import Multiplayer from './components/multiplayer';
import Spectator from './components/spectator';
import Authenticated from './components/authenticated';

const routes = [
{
Expand All @@ -21,6 +22,11 @@ const routes = [
text: 'Multiplayer',
component: Multiplayer,
},
{
path: '/authenticated',
text: 'Authenticated',
component: Authenticated,
},
{
path: '/spectator',
text: 'Spectator',
Expand Down
1 change: 1 addition & 0 deletions examples/react/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'path';
import KoaStatic from 'koa-static';
import KoaHelmet from 'koa-helmet';
import KoaWebpack from 'koa-webpack';

import WebpackConfig from './webpack.dev.js';
import { Server } from 'boardgame.io/server';
import TicTacToe from './modules/tic-tac-toe/game';
Expand Down
6 changes: 5 additions & 1 deletion examples/react/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ const OpenBrowserPlugin = require('open-browser-webpack-plugin');
const port = process.env.PORT || 8000;

module.exports = {
entry: ['webpack-hot-middleware/client', path.resolve(__dirname, 'index.js')],
entry: [
'babel-polyfill',
'webpack-hot-middleware/client',
path.resolve(__dirname, 'index.js'),
],

output: {
publicPath: '/',
Expand Down
Loading

0 comments on commit 8e2f8c4

Please sign in to comment.