Skip to content

Commit afdb79e

Browse files
delucisnicolodavis
andcommitted
refactor: Harmonise Master’s auth signature with authenticateCredentials (#539)
* test(server): Add coverage for the `authenticateCredentials` wrapper * feat(server): Don’t wrap user-provided authentication method * refactor(master): Call auth method with (credentials, playerMetadata) User-provided and default `auth` functions are now called with (credentials, playerMetadata). Checks in the default `auth` implementation that established whether or not we should authenticate have been split into a separate `doesGameRequireAuthentication` method, that is always used regardless of user-defined `auth` functions. * refactor(master): Account for undefined metadata in default auth method If a user were to dispatch an action with a non-existent playerID, we might plausibly get an undefined `playerMetadata` argument. * feat(master): Skip doesGameRequireAuthentication check for custom auth * fix(master): Safely retrieve player metadata Account for undefined parameters when getting player metadata to pass to auth method * test(master): Remove metadata from authentication test * test(master): Improve coverage * refactor(master): Don’t explicitly return undefined in getPlayerMetadata * refactor(master): Rename `credentials` parameter to `actionCredentials` * refactor(master): Tweak how the auth & shouldAuth fields are initialised If the `auth` parameter is not passed as `true` or a custom function, there is no need for the `shouldAuth` function to do anything except always return `false`. Co-authored-by: Nicolo John Davis <[email protected]>
1 parent 5c9430f commit afdb79e

3 files changed

Lines changed: 139 additions & 131 deletions

File tree

src/master/master.js

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import * as logging from '../core/logger';
1515

1616
const GameMetadataKey = gameID => `${gameID}:metadata`;
1717

18+
export const getPlayerMetadata = (gameMetadata, playerID) => {
19+
if (gameMetadata && gameMetadata.players) {
20+
return gameMetadata.players[playerID];
21+
}
22+
};
23+
1824
/**
1925
* Redact the log.
2026
*
@@ -53,42 +59,27 @@ export function redactLog(log, playerID) {
5359
}
5460

5561
/**
56-
* Verifies that the move came from a player with the
57-
* appropriate credentials.
62+
* Verifies that the game has metadata and is using credentials.
5863
*/
59-
export const isActionFromAuthenticPlayer = ({
60-
action,
61-
gameMetadata,
62-
playerID,
63-
}) => {
64-
if (!gameMetadata) {
65-
return true;
66-
}
67-
68-
const hasCredentials = Object.keys(gameMetadata.players).some(key => {
69-
return !!(
70-
gameMetadata.players[key] && gameMetadata.players[key].credentials
71-
);
64+
export const doesGameRequireAuthentication = gameMetadata => {
65+
if (!gameMetadata) return false;
66+
const { players } = gameMetadata;
67+
const hasCredentials = Object.keys(players).some(key => {
68+
return !!(players[key] && players[key].credentials);
7269
});
73-
if (!hasCredentials) {
74-
return true;
75-
}
76-
77-
if (!action.payload) {
78-
return false;
79-
}
80-
81-
if (!action.payload.credentials) {
82-
return false;
83-
}
84-
85-
if (
86-
action.payload.credentials !== gameMetadata.players[playerID].credentials
87-
) {
88-
return false;
89-
}
70+
return hasCredentials;
71+
};
9072

91-
return true;
73+
/**
74+
* Verifies that the move came from a player with the correct credentials.
75+
*/
76+
export const isActionFromAuthenticPlayer = (
77+
actionCredentials,
78+
playerMetadata
79+
) => {
80+
if (!actionCredentials) return false;
81+
if (!playerMetadata) return false;
82+
return actionCredentials === playerMetadata.credentials;
9283
};
9384

9485
/**
@@ -104,11 +95,14 @@ export class Master {
10495
this.storageAPI = storageAPI;
10596
this.transportAPI = transportAPI;
10697
this.auth = () => true;
98+
this.shouldAuth = () => false;
10799

108100
if (auth === true) {
109101
this.auth = isActionFromAuthenticPlayer;
102+
this.shouldAuth = doesGameRequireAuthentication;
110103
} else if (typeof auth === 'function') {
111104
this.auth = auth;
105+
this.shouldAuth = () => true;
112106
}
113107
}
114108

@@ -119,21 +113,19 @@ export class Master {
119113
*/
120114
async onUpdate(action, stateID, gameID, playerID) {
121115
let isActionAuthentic;
122-
116+
const { credentials } = action.payload || {};
123117
if (this.executeSynchronously) {
124118
const gameMetadata = this.storageAPI.get(GameMetadataKey(gameID));
125-
isActionAuthentic = this.auth({
126-
action,
127-
gameMetadata,
128-
playerID,
129-
});
119+
const playerMetadata = getPlayerMetadata(gameMetadata, playerID);
120+
isActionAuthentic = this.shouldAuth(gameMetadata)
121+
? this.auth(credentials, playerMetadata)
122+
: true;
130123
} else {
131124
const gameMetadata = await this.storageAPI.get(GameMetadataKey(gameID));
132-
isActionAuthentic = await this.auth({
133-
action,
134-
gameMetadata,
135-
playerID,
136-
});
125+
const playerMetadata = getPlayerMetadata(gameMetadata, playerID);
126+
isActionAuthentic = this.shouldAuth(gameMetadata)
127+
? await this.auth(credentials, playerMetadata)
128+
: true;
137129
}
138130
if (!isActionAuthentic) {
139131
return { error: 'unauthorized action' };

src/master/master.test.js

Lines changed: 102 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
import * as ActionCreators from '../core/action-creators';
1010
import { InMemory } from '../server/db/inmemory';
11-
import { Master, redactLog, isActionFromAuthenticPlayer } from './master';
11+
import {
12+
Master,
13+
redactLog,
14+
getPlayerMetadata,
15+
doesGameRequireAuthentication,
16+
isActionFromAuthenticPlayer,
17+
} from './master';
1218
import { error } from '../core/logger';
1319

1420
jest.mock('../core/logger', () => ({
@@ -258,12 +264,13 @@ describe('authentication', () => {
258264
const send = jest.fn();
259265
const sendAll = jest.fn();
260266
const game = { seed: 0 };
267+
const gameID = 'gameID';
261268
const action = ActionCreators.gameEvent('endTurn');
262269
const storage = new InMemory();
263270

264271
beforeAll(async () => {
265272
const master = new Master(game, storage, TransportAPI());
266-
await master.onSync('gameID', '0');
273+
await master.onSync(gameID, '0');
267274
});
268275

269276
test('auth failure', async () => {
@@ -274,7 +281,7 @@ describe('authentication', () => {
274281
TransportAPI(send, sendAll),
275282
isActionFromAuthenticPlayer
276283
);
277-
await master.onUpdate(action, 0, 'gameID', '0');
284+
await master.onUpdate(action, 0, gameID, '0');
278285
expect(sendAll).not.toHaveBeenCalled();
279286
});
280287

@@ -286,13 +293,13 @@ describe('authentication', () => {
286293
TransportAPI(send, sendAll),
287294
isActionFromAuthenticPlayer
288295
);
289-
await master.onUpdate(action, 0, 'gameID', '0');
296+
await master.onUpdate(action, 0, gameID, '0');
290297
expect(sendAll).toHaveBeenCalled();
291298
});
292299

293300
test('default', async () => {
294301
const master = new Master(game, storage, TransportAPI(send, sendAll), true);
295-
await master.onUpdate(action, 0, 'gameID', '0');
302+
await master.onUpdate(action, 0, gameID, '0');
296303
expect(sendAll).toHaveBeenCalled();
297304
});
298305
});
@@ -420,130 +427,147 @@ describe('redactLog', () => {
420427
});
421428
});
422429

423-
describe('isActionFromAuthenticPlayer', () => {
424-
let action;
425-
let playerID;
426-
let gameMetadata;
427-
428-
beforeEach(() => {
429-
playerID = '0';
430-
431-
action = {
432-
payload: {
433-
credentials: 'SECRET',
434-
},
435-
};
430+
describe('getPlayerMetadata', () => {
431+
describe('when metadata is not found', () => {
432+
test('then playerMetadata is undefined', () => {
433+
expect(getPlayerMetadata(undefined, '0')).toBeUndefined();
434+
});
435+
});
436436

437-
gameMetadata = {
438-
players: {
439-
'0': {
440-
credentials: 'SECRET',
441-
},
442-
},
443-
};
437+
describe('when metadata does not contain players field', () => {
438+
test('then playerMetadata is undefined', () => {
439+
expect(getPlayerMetadata({}, '0')).toBeUndefined();
440+
});
444441
});
445442

446-
describe('when game metadata is not found', () => {
447-
beforeEach(() => {
448-
gameMetadata = null;
443+
describe('when metadata does not contain playerID', () => {
444+
test('then playerMetadata is undefined', () => {
445+
expect(getPlayerMetadata({ players: { '1': {} } }, '0')).toBeUndefined();
449446
});
447+
});
450448

451-
test('the action is authentic', () => {
452-
const result = isActionFromAuthenticPlayer({
453-
action,
454-
gameMetadata,
455-
playerID,
456-
});
449+
describe('when metadata contains playerID', () => {
450+
test('then playerMetadata is returned', () => {
451+
const playerMetadata = { credentials: 'SECRET' };
452+
const result = getPlayerMetadata(
453+
{ players: { '0': playerMetadata } },
454+
'0'
455+
);
456+
expect(result).toBe(playerMetadata);
457+
});
458+
});
459+
});
457460

458-
expect(result).toBeTruthy();
461+
describe('doesGameRequireAuthentication', () => {
462+
describe('when game metadata is not found', () => {
463+
test('then authentication is not required', () => {
464+
const result = doesGameRequireAuthentication();
465+
expect(result).toBe(false);
459466
});
460467
});
461468

462469
describe('when game has no credentials', () => {
463-
beforeEach(() => {
464-
gameMetadata = {
470+
test('then authentication is not required', () => {
471+
const gameMetadata = {
465472
players: {
466473
'0': {},
467474
},
468475
};
476+
const result = doesGameRequireAuthentication(gameMetadata);
477+
expect(result).toBe(false);
469478
});
479+
});
470480

471-
test('then action is authentic', async () => {
472-
const result = isActionFromAuthenticPlayer({
473-
action,
474-
gameMetadata,
475-
playerID,
476-
});
477-
478-
expect(result).toBeTruthy();
481+
describe('when game has credentials', () => {
482+
test('then authentication is required', () => {
483+
const gameMetadata = {
484+
players: {
485+
'0': {
486+
credentials: 'SECRET',
487+
},
488+
},
489+
};
490+
const result = doesGameRequireAuthentication(gameMetadata);
491+
expect(result).toBe(true);
479492
});
480493
});
494+
});
495+
496+
describe('isActionFromAuthenticPlayer', () => {
497+
let action;
498+
let playerID;
499+
let gameMetadata;
500+
let credentials;
501+
let playerMetadata;
502+
503+
beforeEach(() => {
504+
playerID = '0';
505+
506+
action = {
507+
payload: { credentials: 'SECRET' },
508+
};
509+
510+
gameMetadata = {
511+
players: {
512+
'0': { credentials: 'SECRET' },
513+
},
514+
};
515+
516+
playerMetadata = gameMetadata.players[playerID];
517+
({ credentials } = action.payload || {});
518+
});
481519

482520
describe('when game has credentials', () => {
483521
describe('when action contains no payload', () => {
484522
beforeEach(() => {
485523
action = {};
524+
({ credentials } = action.payload || {});
486525
});
487526

488527
test('the action is not authentic', async () => {
489-
const result = isActionFromAuthenticPlayer({
490-
action,
491-
gameMetadata,
492-
playerID,
493-
});
494-
495-
expect(result).toBeFalsy();
528+
const result = isActionFromAuthenticPlayer(credentials, playerMetadata);
529+
expect(result).toBe(false);
496530
});
497531
});
498532

499533
describe('when action contains no credentials', () => {
500534
beforeEach(() => {
501535
action = {
502-
payload: {
503-
someStuff: 'foo',
504-
},
536+
payload: { someStuff: 'foo' },
505537
};
538+
({ credentials } = action.payload || {});
506539
});
507540

508541
test('then action is not authentic', async () => {
509-
const result = isActionFromAuthenticPlayer({
510-
action,
511-
gameMetadata,
512-
playerID,
513-
});
514-
515-
expect(result).toBeFalsy();
542+
const result = isActionFromAuthenticPlayer(credentials, playerMetadata);
543+
expect(result).toBe(false);
516544
});
517545
});
518546

519547
describe('when action credentials do not match game credentials', () => {
520548
beforeEach(() => {
521549
action = {
522-
payload: {
523-
credentials: 'WRONG',
524-
},
550+
payload: { credentials: 'WRONG' },
525551
};
552+
({ credentials } = action.payload || {});
526553
});
527554
test('then action is not authentic', async () => {
528-
const result = isActionFromAuthenticPlayer({
529-
action,
530-
gameMetadata,
531-
playerID,
532-
});
555+
const result = isActionFromAuthenticPlayer(credentials, playerMetadata);
556+
expect(result).toBe(false);
557+
});
558+
});
533559

534-
expect(result).toBeFalsy();
560+
describe('when playerMetadata is not found', () => {
561+
test('then action is not authentic', () => {
562+
const result = isActionFromAuthenticPlayer(credentials);
563+
expect(result).toBe(false);
535564
});
536565
});
537566

538567
describe('when action credentials do match game credentials', () => {
539568
test('then action is authentic', async () => {
540-
const result = isActionFromAuthenticPlayer({
541-
action,
542-
gameMetadata,
543-
playerID,
544-
});
545-
546-
expect(result).toBeTruthy();
569+
const result = isActionFromAuthenticPlayer(credentials, playerMetadata);
570+
expect(result).toBe(true);
547571
});
548572
});
549573
});

0 commit comments

Comments
 (0)