passport-steam-openid 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +1 -1
- package/.github/workflows/pull-request.yml +6 -0
- package/.github/workflows/release.yml +6 -0
- package/README.md +2 -0
- package/dist/strategy.js +1 -1
- package/package.json +22 -3
- package/sample/package-lock.json +9 -9
- package/src/strategy.ts +1 -1
- package/test/integration/setup/data.ts +30 -0
- package/test/integration/setup/server.ts +89 -0
- package/test/integration/strategy.ts +121 -0
- package/test/setup/data.ts +41 -0
- package/test/strategy.ts +725 -0
- package/tsconfig.json +1 -1
package/.eslintrc.js
CHANGED
|
@@ -15,7 +15,7 @@ module.exports = {
|
|
|
15
15
|
node: true,
|
|
16
16
|
jest: true,
|
|
17
17
|
},
|
|
18
|
-
ignorePatterns: ['.eslintrc.js', 'sample/*'],
|
|
18
|
+
ignorePatterns: ['.eslintrc.js', 'sample/*', 'test/*'],
|
|
19
19
|
rules: {
|
|
20
20
|
'@typescript-eslint/interface-name-prefix': 'off',
|
|
21
21
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@ Passport strategy for authenticating with steam openid without the use of 3rd pa
|
|
|
3
3
|
which have been proved to be source of many exploits of steam openid system, apparently by design.
|
|
4
4
|
This package only relies on [passport](https://www.passportjs.org/) and [axios](https://axios-http.com/).
|
|
5
5
|
|
|
6
|
+
Library is fully covered with tests, both unit and integration tests to make sure everything runs correctly.
|
|
7
|
+
|
|
6
8
|
## Usage
|
|
7
9
|
```ts
|
|
8
10
|
import { SteamOpenIdStrategy } from 'passport-steam-openid';
|
package/dist/strategy.js
CHANGED
|
@@ -286,7 +286,7 @@ class SteamOpenIdStrategy extends passport_1.Strategy {
|
|
|
286
286
|
steamids: steamId,
|
|
287
287
|
key: this.apiKey,
|
|
288
288
|
};
|
|
289
|
-
return
|
|
289
|
+
return this.axios
|
|
290
290
|
.get(`${constant_1.PLAYER_SUMMARY_URL}/?${querystring_1.default.stringify(summaryQuery)}`)
|
|
291
291
|
.then(({ data }) => {
|
|
292
292
|
var _a;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "passport-steam-openid",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Passport strategy for authenticating with steam openid without the use of 3rd party openid packages.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
"build": "npx tsc",
|
|
9
9
|
"format": "npx prettier --write \"src/**/*.ts\"",
|
|
10
10
|
"lint": "npx eslint \"{src,test}/**/*.ts\" --fix",
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
11
|
"prepare": "npx husky install",
|
|
13
|
-
"semantic-release": "semantic-release"
|
|
12
|
+
"semantic-release": "semantic-release",
|
|
13
|
+
"test": "npx mocha -r ts-node/register 'test/*.ts'",
|
|
14
|
+
"test:integration": "npx mocha -r ts-node/register 'test/integration/*.ts'",
|
|
15
|
+
"test:coverage": "npx nyc --reporter=text \"npm run test:integration && npm run test\"",
|
|
16
|
+
"test:coverage:html": "npx nyc --reporter=html \"npm run test:integration && npm run test\""
|
|
14
17
|
},
|
|
15
18
|
"keywords": [],
|
|
16
19
|
"author": "glencoco",
|
|
@@ -22,19 +25,35 @@
|
|
|
22
25
|
"devDependencies": {
|
|
23
26
|
"@semantic-release/changelog": "^6.0.3",
|
|
24
27
|
"@semantic-release/git": "^10.0.1",
|
|
28
|
+
"@types/chai": "^4.3.5",
|
|
29
|
+
"@types/chai-as-promised": "^7.1.5",
|
|
30
|
+
"@types/express": "^4.17.17",
|
|
31
|
+
"@types/express-session": "^1.17.7",
|
|
32
|
+
"@types/mocha": "^10.0.1",
|
|
25
33
|
"@types/node": "^20.3.1",
|
|
26
34
|
"@types/passport": "^1.0.12",
|
|
35
|
+
"@types/sinon": "^10.0.15",
|
|
27
36
|
"@typescript-eslint/eslint-plugin": "^5.59.11",
|
|
28
37
|
"@typescript-eslint/parser": "^5.59.11",
|
|
38
|
+
"chai": "^4.3.7",
|
|
39
|
+
"chai-as-promised": "^7.1.1",
|
|
40
|
+
"chai-http": "^4.4.0",
|
|
29
41
|
"cz-conventional-changelog": "^3.3.0",
|
|
30
42
|
"eslint": "^8.43.0",
|
|
31
43
|
"eslint-config-prettier": "^8.8.0",
|
|
32
44
|
"eslint-plugin-prettier": "^4.2.1",
|
|
33
45
|
"eslint-plugin-sonarjs": "^0.19.0",
|
|
46
|
+
"express": "^4.18.2",
|
|
47
|
+
"express-session": "^1.17.3",
|
|
34
48
|
"husky": "^8.0.3",
|
|
35
49
|
"lint-staged": "^13.2.2",
|
|
50
|
+
"mocha": "^10.2.0",
|
|
51
|
+
"nock": "^13.3.2",
|
|
52
|
+
"nyc": "^15.1.0",
|
|
36
53
|
"prettier": "^2.8.8",
|
|
37
54
|
"semantic-release": "^21.0.5",
|
|
55
|
+
"sinon": "^15.2.0",
|
|
56
|
+
"ts-node": "^10.9.1",
|
|
38
57
|
"typescript": "^5.1.3"
|
|
39
58
|
},
|
|
40
59
|
"config": {
|
package/sample/package-lock.json
CHANGED
|
@@ -695,9 +695,9 @@
|
|
|
695
695
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
|
696
696
|
},
|
|
697
697
|
"node_modules/axios": {
|
|
698
|
-
"version": "1.
|
|
699
|
-
"resolved": "https://registry.npmjs.org/axios/-/axios-1.
|
|
700
|
-
"integrity": "sha512-
|
|
698
|
+
"version": "1.6.2",
|
|
699
|
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
|
700
|
+
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
|
701
701
|
"dependencies": {
|
|
702
702
|
"follow-redirects": "^1.15.0",
|
|
703
703
|
"form-data": "^4.0.0",
|
|
@@ -1477,9 +1477,9 @@
|
|
|
1477
1477
|
"dev": true
|
|
1478
1478
|
},
|
|
1479
1479
|
"node_modules/follow-redirects": {
|
|
1480
|
-
"version": "1.15.
|
|
1481
|
-
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.
|
|
1482
|
-
"integrity": "sha512-
|
|
1480
|
+
"version": "1.15.4",
|
|
1481
|
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
|
1482
|
+
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
|
1483
1483
|
"funding": [
|
|
1484
1484
|
{
|
|
1485
1485
|
"type": "individual",
|
|
@@ -2658,9 +2658,9 @@
|
|
|
2658
2658
|
}
|
|
2659
2659
|
},
|
|
2660
2660
|
"node_modules/word-wrap": {
|
|
2661
|
-
"version": "1.2.
|
|
2662
|
-
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.
|
|
2663
|
-
"integrity": "sha512-
|
|
2661
|
+
"version": "1.2.4",
|
|
2662
|
+
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
|
2663
|
+
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
|
2664
2664
|
"dev": true,
|
|
2665
2665
|
"engines": {
|
|
2666
2666
|
"node": ">=0.10.0"
|
package/src/strategy.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { VALID_NONCE, VALID_OPENID_ENDPOINT } from '../../../src';
|
|
2
|
+
|
|
3
|
+
export const STEAMID = '76561197960435530';
|
|
4
|
+
export const SUCCESSFUL_QUERY: Record<string, string> = {
|
|
5
|
+
'openid.mode': 'id_res',
|
|
6
|
+
'openid.ns': VALID_NONCE,
|
|
7
|
+
'openid.identity': `https://steamcommunity.com/openid/id/${STEAMID}`,
|
|
8
|
+
'openid.claimed_id': `https://steamcommunity.com/openid/id/${STEAMID}`,
|
|
9
|
+
'openid.return_to': '/auth/steam',
|
|
10
|
+
'openid.op_endpoint': VALID_OPENID_ENDPOINT,
|
|
11
|
+
'openid.response_nonce': `${new Date().toJSON()}8df86bac92ad1addaf3735a5aabdc6e2a7`,
|
|
12
|
+
'openid.assoc_handle': '1234567890',
|
|
13
|
+
'openid.signed':
|
|
14
|
+
'signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle',
|
|
15
|
+
'openid.sig': 'dc6e2a79de2c6aceac495ad5f4c6b6e0bfe30',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function validateBody(body: Record<string, string>) {
|
|
19
|
+
const queryKeys = Object.keys(SUCCESSFUL_QUERY);
|
|
20
|
+
const bodyKeys = Object.keys(body);
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < queryKeys.length; i++) {
|
|
23
|
+
const key: string = queryKeys[i] as string;
|
|
24
|
+
if (key !== bodyKeys[i] && SUCCESSFUL_QUERY[key] != body[key]) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import express, { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import passport from 'passport';
|
|
3
|
+
import session from 'express-session';
|
|
4
|
+
import {
|
|
5
|
+
SteamOpenIdError,
|
|
6
|
+
SteamOpenIdStrategy,
|
|
7
|
+
SteamOpenIdUser,
|
|
8
|
+
} from '../../../src';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Exported server for superagent.
|
|
12
|
+
*
|
|
13
|
+
* This server acts as a basic implementation
|
|
14
|
+
* of express server with passport authentication,
|
|
15
|
+
* using a single endpoint that handles everything
|
|
16
|
+
* via the provided strategy.
|
|
17
|
+
*
|
|
18
|
+
* This configuration does not fetch user information from the API.
|
|
19
|
+
*
|
|
20
|
+
* Error handling hides all important details of this implementation.
|
|
21
|
+
* Other important factors should be handled by helmet.js
|
|
22
|
+
*/
|
|
23
|
+
export const server = express();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Type definition for user property on request object.
|
|
27
|
+
*
|
|
28
|
+
* Only required for the response from successful authentication.
|
|
29
|
+
*/
|
|
30
|
+
declare module 'express-serve-static-core' {
|
|
31
|
+
interface Request {
|
|
32
|
+
user?: SteamOpenIdUser | undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Server setup includes:
|
|
38
|
+
* - passport strategy initialization
|
|
39
|
+
* - passport initialization
|
|
40
|
+
* - session middleware
|
|
41
|
+
* - auth endpoint setup
|
|
42
|
+
* - error handling
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
passport.use(
|
|
46
|
+
new SteamOpenIdStrategy(
|
|
47
|
+
{
|
|
48
|
+
profile: false,
|
|
49
|
+
returnURL: `/auth/steam`,
|
|
50
|
+
},
|
|
51
|
+
(_req, _steamId, profile, done) => {
|
|
52
|
+
done(null, profile);
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
passport.serializeUser((user: any, done) => {
|
|
58
|
+
done(null, user.steamid);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
passport.deserializeUser((user: any, done) => {
|
|
62
|
+
done(null, { steamid: user });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
server.use(
|
|
66
|
+
session({
|
|
67
|
+
secret: 'secret',
|
|
68
|
+
resave: true,
|
|
69
|
+
saveUninitialized: true,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
server.use(passport.initialize());
|
|
73
|
+
server.use(passport.session());
|
|
74
|
+
|
|
75
|
+
server.get('/auth/steam', passport.authenticate('steam-openid'), (req, res) => {
|
|
76
|
+
res.status(200).send(`Authenticated as ${req.user?.steamid}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
server.use(
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
81
|
+
(err: Error, _req: Request, res: Response, _next: NextFunction): void => {
|
|
82
|
+
if (err instanceof SteamOpenIdError) {
|
|
83
|
+
res.status(401).send('Unauthorized');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
res.status(500).send('Internal Server Error.');
|
|
88
|
+
},
|
|
89
|
+
);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import chai, { expect } from 'chai';
|
|
2
|
+
import chaiHttp from 'chai-http';
|
|
3
|
+
import nock from 'nock';
|
|
4
|
+
|
|
5
|
+
import { server } from './setup/server';
|
|
6
|
+
import { STEAMID, SUCCESSFUL_QUERY, validateBody } from './setup/data';
|
|
7
|
+
import {
|
|
8
|
+
VALID_ID_SELECT,
|
|
9
|
+
VALID_NONCE,
|
|
10
|
+
VALID_OPENID_ENDPOINT,
|
|
11
|
+
} from '../../src/constant';
|
|
12
|
+
|
|
13
|
+
chai.use(chaiHttp);
|
|
14
|
+
chai.should();
|
|
15
|
+
|
|
16
|
+
describe('SteamOpenIdStrategy Integration Test', () => {
|
|
17
|
+
it('Successfully receives a redirect from steam', (done) => {
|
|
18
|
+
const response = '<h1>Successful redirect to steam</h1>';
|
|
19
|
+
|
|
20
|
+
nock('https://steamcommunity.com')
|
|
21
|
+
.get('/openid/login')
|
|
22
|
+
.query((query) => {
|
|
23
|
+
return (
|
|
24
|
+
query['openid.mode'] === 'checkid_setup' &&
|
|
25
|
+
query['openid.ns'] === VALID_NONCE &&
|
|
26
|
+
query['openid.identity'] === VALID_ID_SELECT &&
|
|
27
|
+
query['openid.claimed_id'] === VALID_ID_SELECT &&
|
|
28
|
+
query['openid.return_to'] === '/auth/steam'
|
|
29
|
+
);
|
|
30
|
+
})
|
|
31
|
+
.reply(200, response, {
|
|
32
|
+
'Content-Type': 'text/plain',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
chai
|
|
36
|
+
.request(server)
|
|
37
|
+
.get('/auth/steam')
|
|
38
|
+
.redirects(1)
|
|
39
|
+
.end((err, res) => {
|
|
40
|
+
if (err) {
|
|
41
|
+
done(err);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
res.should.redirectTo(new RegExp(`^${VALID_OPENID_ENDPOINT}`));
|
|
46
|
+
res.should.have.status(200);
|
|
47
|
+
expect(res.type).equal('text/plain');
|
|
48
|
+
expect(res.text).equal(response);
|
|
49
|
+
|
|
50
|
+
done();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Successfully authenticates a valid user', (done) => {
|
|
55
|
+
nock('https://steamcommunity.com')
|
|
56
|
+
.post('/openid/login', validateBody)
|
|
57
|
+
.matchHeader('Content-Type', 'application/x-www-form-urlencoded')
|
|
58
|
+
.reply(200, 'ns:http://specs.openid.net/auth/2.0\nis_valid:true\n');
|
|
59
|
+
|
|
60
|
+
chai
|
|
61
|
+
.request(server)
|
|
62
|
+
.get('/auth/steam')
|
|
63
|
+
.query(SUCCESSFUL_QUERY)
|
|
64
|
+
.redirects(0)
|
|
65
|
+
.end((err, res) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
done(err);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.should.have.status(200);
|
|
72
|
+
expect(res.text).equal(`Authenticated as ${STEAMID}`);
|
|
73
|
+
|
|
74
|
+
done();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Fails due to invalid response', () => {
|
|
79
|
+
it('Has invalid response nonce', (done) => {
|
|
80
|
+
nock('https://steamcommunity.com')
|
|
81
|
+
.post('/openid/login', validateBody)
|
|
82
|
+
.reply(200, 'ns:http://specs.openid.net/auth/1.0\nis_valid:true\n');
|
|
83
|
+
|
|
84
|
+
chai
|
|
85
|
+
.request(server)
|
|
86
|
+
.get('/auth/steam')
|
|
87
|
+
.query(SUCCESSFUL_QUERY)
|
|
88
|
+
.redirects(0)
|
|
89
|
+
.end((err, res) => {
|
|
90
|
+
if (err) {
|
|
91
|
+
done(err);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
res.should.have.status(401);
|
|
96
|
+
done();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('Has is_valid set to false', (done) => {
|
|
101
|
+
nock('https://steamcommunity.com')
|
|
102
|
+
.post('/openid/login', validateBody)
|
|
103
|
+
.reply(200, 'ns:http://specs.openid.net/auth/2.0\nis_valid:false\n');
|
|
104
|
+
|
|
105
|
+
chai
|
|
106
|
+
.request(server)
|
|
107
|
+
.get('/auth/steam')
|
|
108
|
+
.query(SUCCESSFUL_QUERY)
|
|
109
|
+
.redirects(0)
|
|
110
|
+
.end((err, res) => {
|
|
111
|
+
if (err) {
|
|
112
|
+
done(err);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
res.should.have.status(401);
|
|
117
|
+
done();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { VALID_NONCE, VALID_OPENID_ENDPOINT } from '../../src';
|
|
2
|
+
|
|
3
|
+
export const RETURN_URL = '/auth/steam';
|
|
4
|
+
|
|
5
|
+
export const query: {
|
|
6
|
+
properties: Record<string, string>;
|
|
7
|
+
get(): Record<string, string>;
|
|
8
|
+
change(change: Record<string, string>): Record<string, string>;
|
|
9
|
+
remove(property: string): Record<string, string>;
|
|
10
|
+
} = {
|
|
11
|
+
/**
|
|
12
|
+
* Valid query properties
|
|
13
|
+
*/
|
|
14
|
+
properties: {
|
|
15
|
+
'openid.mode': 'id_res',
|
|
16
|
+
'openid.ns': VALID_NONCE,
|
|
17
|
+
'openid.identity': `https://steamcommunity.com/openid/id/76561197960435530`,
|
|
18
|
+
'openid.claimed_id': `https://steamcommunity.com/openid/id/76561197960435530`,
|
|
19
|
+
'openid.return_to': RETURN_URL,
|
|
20
|
+
'openid.op_endpoint': VALID_OPENID_ENDPOINT,
|
|
21
|
+
'openid.response_nonce': `${new Date().toJSON()}8df86bac92ad1addaf3735a5aabdc6e2a7`,
|
|
22
|
+
'openid.assoc_handle': '1234567890',
|
|
23
|
+
'openid.signed':
|
|
24
|
+
'signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle',
|
|
25
|
+
'openid.sig': 'dc6e2a79de2c6aceac495ad5f4c6b6e0bfe30',
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
get() {
|
|
29
|
+
return { ...this.properties };
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
change(change: Record<string, string>) {
|
|
33
|
+
return { ...this.get(), ...change };
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
remove(property: string) {
|
|
37
|
+
const properties = this.get();
|
|
38
|
+
delete properties[property];
|
|
39
|
+
return properties;
|
|
40
|
+
},
|
|
41
|
+
};
|
package/test/strategy.ts
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import qs from 'querystring';
|
|
2
|
+
import sinon from 'sinon';
|
|
3
|
+
import chaiAsPromised from 'chai-as-promised';
|
|
4
|
+
import chai, { expect } from 'chai';
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import {
|
|
7
|
+
SteamOpenIdError,
|
|
8
|
+
SteamOpenIdErrorType,
|
|
9
|
+
SteamOpenIdStrategy,
|
|
10
|
+
SteamOpenIdStrategyOptionsWithoutProfile,
|
|
11
|
+
SteamOpenIdUser,
|
|
12
|
+
VALID_ID_SELECT,
|
|
13
|
+
VALID_NONCE,
|
|
14
|
+
OPENID_QUERY_PROPS,
|
|
15
|
+
VALID_OPENID_ENDPOINT,
|
|
16
|
+
PLAYER_SUMMARY_URL,
|
|
17
|
+
} from '../src';
|
|
18
|
+
import { RETURN_URL, query } from './setup/data';
|
|
19
|
+
|
|
20
|
+
chai.use(chaiAsPromised);
|
|
21
|
+
|
|
22
|
+
describe('SteamOpenIdStrategy Unit Test', () => {
|
|
23
|
+
let strategy: SteamOpenIdStrategy<
|
|
24
|
+
SteamOpenIdStrategyOptionsWithoutProfile,
|
|
25
|
+
SteamOpenIdUser
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
strategy = new SteamOpenIdStrategy({
|
|
30
|
+
apiKey: 'an api key',
|
|
31
|
+
profile: false,
|
|
32
|
+
returnURL: RETURN_URL,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('authenticate', () => {
|
|
37
|
+
let successStub: sinon.SinonStub;
|
|
38
|
+
let failStub: sinon.SinonStub;
|
|
39
|
+
let errorStub: sinon.SinonStub;
|
|
40
|
+
let redirectStub: sinon.SinonStub;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
strategy.success = () => null;
|
|
44
|
+
strategy.fail = () => null;
|
|
45
|
+
strategy.error = () => null;
|
|
46
|
+
strategy.redirect = () => null;
|
|
47
|
+
|
|
48
|
+
successStub = sinon.stub(strategy, 'success');
|
|
49
|
+
failStub = sinon.stub(strategy, 'fail');
|
|
50
|
+
errorStub = sinon.stub(strategy, 'error');
|
|
51
|
+
redirectStub = sinon.stub(strategy, 'redirect');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Authenticates user without verify function', async () => {
|
|
55
|
+
const request = {};
|
|
56
|
+
const user = { steamid: '76561197960435530' };
|
|
57
|
+
|
|
58
|
+
const handleRequestStub = sinon
|
|
59
|
+
.stub(strategy, 'handleRequest')
|
|
60
|
+
.resolves(user);
|
|
61
|
+
|
|
62
|
+
await strategy.authenticate(request);
|
|
63
|
+
|
|
64
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
65
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
66
|
+
expect(successStub.callCount).equal(1);
|
|
67
|
+
expect(successStub.calledWithExactly(user)).equal(true);
|
|
68
|
+
expect(failStub.callCount).equal(0);
|
|
69
|
+
expect(errorStub.callCount).equal(0);
|
|
70
|
+
expect(redirectStub.callCount).equal(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('Authenticates and calls verify function', async () => {
|
|
74
|
+
strategy['verify'] = (_, __, user, callback) => {
|
|
75
|
+
callback(null, user);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const request = {};
|
|
79
|
+
const user = { steamid: '76561197960435530' };
|
|
80
|
+
|
|
81
|
+
const verifySpy = sinon.spy(strategy, 'verify' as any);
|
|
82
|
+
const handleRequestStub = sinon
|
|
83
|
+
.stub(strategy, 'handleRequest')
|
|
84
|
+
.resolves(user);
|
|
85
|
+
|
|
86
|
+
await strategy.authenticate(request);
|
|
87
|
+
|
|
88
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
89
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
90
|
+
expect(verifySpy.callCount).equal(1);
|
|
91
|
+
expect(verifySpy.calledWithMatch(request, user.steamid, user)).equal(
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
expect(successStub.callCount).equal(1);
|
|
95
|
+
expect(successStub.calledWithExactly(user)).equal(true);
|
|
96
|
+
expect(failStub.callCount).equal(0);
|
|
97
|
+
expect(errorStub.callCount).equal(0);
|
|
98
|
+
expect(redirectStub.callCount).equal(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('Fails to handle request but gets redirected', async () => {
|
|
102
|
+
const request = {};
|
|
103
|
+
const error = new SteamOpenIdError(
|
|
104
|
+
'Invalid mode',
|
|
105
|
+
SteamOpenIdErrorType.InvalidMode,
|
|
106
|
+
);
|
|
107
|
+
const url = 'redirect url';
|
|
108
|
+
|
|
109
|
+
const isRetryableErrorStub = sinon
|
|
110
|
+
.stub(strategy as any, 'isRetryableError')
|
|
111
|
+
.returns(true);
|
|
112
|
+
const buildRedirectUrlStub = sinon
|
|
113
|
+
.stub(strategy as any, 'buildRedirectUrl')
|
|
114
|
+
.returns(url);
|
|
115
|
+
const handleRequestStub = sinon
|
|
116
|
+
.stub(strategy, 'handleRequest')
|
|
117
|
+
.rejects(error);
|
|
118
|
+
|
|
119
|
+
await strategy.authenticate(request);
|
|
120
|
+
|
|
121
|
+
expect(isRetryableErrorStub.callCount).equal(1);
|
|
122
|
+
expect(isRetryableErrorStub.calledWithExactly(error)).equal(true);
|
|
123
|
+
expect(buildRedirectUrlStub.callCount).equal(1);
|
|
124
|
+
expect(buildRedirectUrlStub.calledWithExactly()).equal(true);
|
|
125
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
126
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
127
|
+
expect(redirectStub.callCount).equal(1);
|
|
128
|
+
expect(redirectStub.calledWithExactly(url)).equal(true);
|
|
129
|
+
expect(errorStub.callCount).equal(0);
|
|
130
|
+
expect(failStub.callCount).equal(0);
|
|
131
|
+
expect(successStub.callCount).equal(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('Fails to handle request and terminates', async () => {
|
|
135
|
+
const request = {};
|
|
136
|
+
const error = new Error('Not retryable');
|
|
137
|
+
|
|
138
|
+
const handleRequestStub = sinon
|
|
139
|
+
.stub(strategy, 'handleRequest')
|
|
140
|
+
.rejects(error);
|
|
141
|
+
|
|
142
|
+
await strategy.authenticate(request);
|
|
143
|
+
|
|
144
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
145
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
146
|
+
expect(errorStub.callCount).equal(1);
|
|
147
|
+
expect(errorStub.calledWithExactly(error)).equal(true);
|
|
148
|
+
expect(failStub.callCount).equal(0);
|
|
149
|
+
expect(successStub.callCount).equal(0);
|
|
150
|
+
expect(redirectStub.callCount).equal(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('Verify function fails', async () => {
|
|
154
|
+
const error = new Error('Verify error');
|
|
155
|
+
strategy['verify'] = (_, __, ___, callback) => {
|
|
156
|
+
callback(error);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const request = {};
|
|
160
|
+
const user = { steamid: '76561197960435530' };
|
|
161
|
+
|
|
162
|
+
const verifySpy = sinon.spy(strategy, 'verify' as any);
|
|
163
|
+
const handleRequestStub = sinon
|
|
164
|
+
.stub(strategy, 'handleRequest')
|
|
165
|
+
.resolves(user);
|
|
166
|
+
|
|
167
|
+
await strategy.authenticate(request);
|
|
168
|
+
|
|
169
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
170
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
171
|
+
expect(verifySpy.callCount).equal(1);
|
|
172
|
+
expect(verifySpy.calledWithMatch(request, user.steamid, user)).equal(
|
|
173
|
+
true,
|
|
174
|
+
);
|
|
175
|
+
expect(errorStub.callCount).equal(1);
|
|
176
|
+
expect(errorStub.calledWithExactly(error)).equal(true);
|
|
177
|
+
expect(failStub.callCount).equal(0);
|
|
178
|
+
expect(successStub.callCount).equal(0);
|
|
179
|
+
expect(redirectStub.callCount).equal(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('Verify returns no user', async () => {
|
|
183
|
+
strategy['verify'] = (_, __, ___, callback) => {
|
|
184
|
+
callback(null, null);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const request = {};
|
|
188
|
+
const user = { steamid: '76561197960435530' };
|
|
189
|
+
|
|
190
|
+
const verifySpy = sinon.spy(strategy, 'verify' as any);
|
|
191
|
+
const handleRequestStub = sinon
|
|
192
|
+
.stub(strategy, 'handleRequest')
|
|
193
|
+
.resolves(user);
|
|
194
|
+
|
|
195
|
+
await strategy.authenticate(request);
|
|
196
|
+
|
|
197
|
+
expect(handleRequestStub.callCount).equal(1);
|
|
198
|
+
expect(handleRequestStub.calledWithExactly(request)).equal(true);
|
|
199
|
+
expect(verifySpy.callCount).equal(1);
|
|
200
|
+
expect(verifySpy.calledWithMatch(request, user.steamid, user)).equal(
|
|
201
|
+
true,
|
|
202
|
+
);
|
|
203
|
+
expect(errorStub.callCount).equal(1);
|
|
204
|
+
expect(failStub.callCount).equal(0);
|
|
205
|
+
expect(successStub.callCount).equal(0);
|
|
206
|
+
expect(redirectStub.callCount).equal(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('handleRequest', () => {
|
|
211
|
+
const request = {};
|
|
212
|
+
const query = {};
|
|
213
|
+
|
|
214
|
+
let getQueryStub: sinon.SinonStub;
|
|
215
|
+
let hasAuthQueryStub: sinon.SinonStub;
|
|
216
|
+
let isQueryValidStub: sinon.SinonStub;
|
|
217
|
+
let validateAgainstSteamStub: sinon.SinonStub;
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
getQueryStub = sinon.stub(strategy as any, 'getQuery').returns(query);
|
|
221
|
+
hasAuthQueryStub = sinon.stub(strategy as any, 'hasAuthQuery');
|
|
222
|
+
isQueryValidStub = sinon.stub(strategy as any, 'isQueryValid');
|
|
223
|
+
validateAgainstSteamStub = sinon.stub(
|
|
224
|
+
strategy as any,
|
|
225
|
+
'validateAgainstSteam',
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
afterEach(() => {
|
|
230
|
+
expect(getQueryStub.callCount).equal(1);
|
|
231
|
+
expect(getQueryStub.calledWithExactly(request)).equal(true);
|
|
232
|
+
expect(hasAuthQueryStub.callCount).equal(1);
|
|
233
|
+
expect(hasAuthQueryStub.calledWithExactly(query)).equal(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('Handles request correctly', async () => {
|
|
237
|
+
hasAuthQueryStub.returns(true);
|
|
238
|
+
isQueryValidStub.returns(true);
|
|
239
|
+
validateAgainstSteamStub.resolves(true);
|
|
240
|
+
|
|
241
|
+
const steamid = '76561197960435530';
|
|
242
|
+
const user = { steamid: '76561197960435530' };
|
|
243
|
+
|
|
244
|
+
const getSteamIdStub = sinon
|
|
245
|
+
.stub(strategy as any, 'getSteamId')
|
|
246
|
+
.returns(steamid);
|
|
247
|
+
const getUserStub = sinon.stub(strategy as any, 'getUser').resolves(user);
|
|
248
|
+
|
|
249
|
+
expect(await strategy.handleRequest(request)).equal(user);
|
|
250
|
+
expect(getSteamIdStub.callCount).equal(1);
|
|
251
|
+
expect(getSteamIdStub.calledWithExactly(query)).equal(true);
|
|
252
|
+
expect(getUserStub.callCount).equal(1);
|
|
253
|
+
expect(getUserStub.calledWithExactly(steamid)).equal(true);
|
|
254
|
+
expect(isQueryValidStub.callCount).equal(1);
|
|
255
|
+
expect(isQueryValidStub.calledWithExactly(query)).equal(true);
|
|
256
|
+
expect(validateAgainstSteamStub.callCount).equal(1);
|
|
257
|
+
expect(validateAgainstSteamStub.calledWithExactly(query)).equal(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('Does not have correct mode', async () => {
|
|
261
|
+
hasAuthQueryStub.returns(false);
|
|
262
|
+
|
|
263
|
+
let err: any;
|
|
264
|
+
try {
|
|
265
|
+
await strategy.handleRequest(request);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
err = e;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
expect(err).to.be.instanceOf(SteamOpenIdError);
|
|
271
|
+
expect(err).to.have.property('code', SteamOpenIdErrorType.InvalidMode);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('Query is invalid', async () => {
|
|
275
|
+
hasAuthQueryStub.returns(true);
|
|
276
|
+
isQueryValidStub.returns(false);
|
|
277
|
+
|
|
278
|
+
let err: any;
|
|
279
|
+
try {
|
|
280
|
+
await strategy.handleRequest(request);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
err = e;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
expect(err).to.be.instanceOf(SteamOpenIdError);
|
|
286
|
+
expect(err).to.have.property('code', SteamOpenIdErrorType.InvalidQuery);
|
|
287
|
+
expect(isQueryValidStub.callCount).equal(1);
|
|
288
|
+
expect(isQueryValidStub.calledWithExactly(query)).equal(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('Steam rejects this authentication request', async () => {
|
|
292
|
+
hasAuthQueryStub.returns(true);
|
|
293
|
+
isQueryValidStub.returns(true);
|
|
294
|
+
validateAgainstSteamStub.resolves(false);
|
|
295
|
+
|
|
296
|
+
let err: any;
|
|
297
|
+
try {
|
|
298
|
+
await strategy.handleRequest(request);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
err = e;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
expect(err).to.be.instanceOf(SteamOpenIdError);
|
|
304
|
+
expect(err).to.have.property('code', SteamOpenIdErrorType.Unauthorized);
|
|
305
|
+
expect(isQueryValidStub.callCount).equal(1);
|
|
306
|
+
expect(isQueryValidStub.calledWithExactly(query)).equal(true);
|
|
307
|
+
expect(validateAgainstSteamStub.callCount).equal(1);
|
|
308
|
+
expect(validateAgainstSteamStub.calledWithExactly(query)).equal(true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('isRetryableError', () => {
|
|
313
|
+
it('Is retriable error', () => {
|
|
314
|
+
const error = new SteamOpenIdError(
|
|
315
|
+
'Message',
|
|
316
|
+
SteamOpenIdErrorType.InvalidMode,
|
|
317
|
+
);
|
|
318
|
+
expect(strategy['isRetryableError'](error)).equal(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('Not an instance of SteamOpenIdError', () => {
|
|
322
|
+
const error = new Error('Message');
|
|
323
|
+
expect(strategy['isRetryableError'](error)).equal(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('Wrong error code', () => {
|
|
327
|
+
const error = new SteamOpenIdError(
|
|
328
|
+
'Message',
|
|
329
|
+
SteamOpenIdErrorType.InvalidSteamId,
|
|
330
|
+
);
|
|
331
|
+
expect(strategy['isRetryableError'](error)).equal(false);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('getQuery', () => {
|
|
336
|
+
it('Returns query from request', () => {
|
|
337
|
+
const request = { query: {} };
|
|
338
|
+
expect(strategy['getQuery'](request)).equal(request.query);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('Fails because query is not present', () => {
|
|
342
|
+
const request = {};
|
|
343
|
+
expect(strategy['getQuery'].bind(strategy, request)).to.throw(Error);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('Fails because query is not an object', () => {
|
|
347
|
+
const request = { query: 'string' };
|
|
348
|
+
expect(strategy['getQuery'].bind(strategy, request)).to.throw(Error);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('hasAuthQuery', () => {
|
|
353
|
+
it('Is an auth query', () => {
|
|
354
|
+
const query = { 'openid.mode': 'id_res' };
|
|
355
|
+
expect(strategy['hasAuthQuery'](query)).equal(true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('Mode is not present', () => {
|
|
359
|
+
const query = {};
|
|
360
|
+
expect(strategy['hasAuthQuery'](query)).equal(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('Wrong mode', () => {
|
|
364
|
+
const query = { 'openid.mode': 'checkid_setup' };
|
|
365
|
+
expect(strategy['hasAuthQuery'](query)).equal(false);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('buildRedirectUrl', () => {
|
|
370
|
+
it('Builds a redirect url to steam', () => {
|
|
371
|
+
const urlString = strategy['buildRedirectUrl']();
|
|
372
|
+
const url = new URL(urlString);
|
|
373
|
+
|
|
374
|
+
expect(url.origin).equal('https://steamcommunity.com');
|
|
375
|
+
expect(url.pathname).equal('/openid/login');
|
|
376
|
+
|
|
377
|
+
const VALID_PARAMS: Record<string, string> = {
|
|
378
|
+
'openid.mode': 'checkid_setup',
|
|
379
|
+
'openid.ns': VALID_NONCE,
|
|
380
|
+
'openid.identity': VALID_ID_SELECT,
|
|
381
|
+
'openid.claimed_id': VALID_ID_SELECT,
|
|
382
|
+
'openid.return_to': RETURN_URL,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
url.searchParams.forEach((value, name) => {
|
|
386
|
+
if (VALID_PARAMS[name] !== value) {
|
|
387
|
+
throw new Error(`Unknown param: ${name} = ${value}.`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('isQueryValid', () => {
|
|
394
|
+
let isValidIdentityStub: sinon.SinonStub;
|
|
395
|
+
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
isValidIdentityStub = sinon.stub(strategy as any, 'isValidIdentity');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('Is valid query', () => {
|
|
401
|
+
isValidIdentityStub.returns(true);
|
|
402
|
+
expect(strategy['isQueryValid'](query.get())).equal(true);
|
|
403
|
+
expect(isValidIdentityStub.callCount).equal(1);
|
|
404
|
+
expect(
|
|
405
|
+
isValidIdentityStub.calledWithExactly(
|
|
406
|
+
query.properties['openid.claimed_id'],
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('Property is missing', () => {
|
|
412
|
+
for (const property of OPENID_QUERY_PROPS) {
|
|
413
|
+
expect(strategy['isQueryValid'](query.remove(property))).equal(false);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('Invalid nonce', () => {
|
|
418
|
+
expect(
|
|
419
|
+
strategy['isQueryValid'](query.change({ 'openid.ns': 'content' })),
|
|
420
|
+
).equal(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('Invalid endpoint', () => {
|
|
424
|
+
expect(
|
|
425
|
+
strategy['isQueryValid'](
|
|
426
|
+
query.change({ 'openid.op_endpoint': 'content' }),
|
|
427
|
+
),
|
|
428
|
+
).equal(false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("ClaimedId and Identity don' match", () => {
|
|
432
|
+
expect(
|
|
433
|
+
strategy['isQueryValid'](
|
|
434
|
+
query.change({ 'openid.claimed_id': 'content' }),
|
|
435
|
+
),
|
|
436
|
+
).equal(false);
|
|
437
|
+
expect(isValidIdentityStub.callCount).equal(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('ClaimedId is invalid', () => {
|
|
441
|
+
isValidIdentityStub.returns(false);
|
|
442
|
+
const claimedIdValue = 'content';
|
|
443
|
+
expect(
|
|
444
|
+
strategy['isQueryValid'](
|
|
445
|
+
query.change({
|
|
446
|
+
'openid.claimed_id': claimedIdValue,
|
|
447
|
+
'openid.identity': claimedIdValue,
|
|
448
|
+
}),
|
|
449
|
+
),
|
|
450
|
+
).equal(false);
|
|
451
|
+
expect(isValidIdentityStub.callCount).equal(1);
|
|
452
|
+
expect(isValidIdentityStub.calledWithExactly(claimedIdValue));
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('Return does not match', () => {
|
|
456
|
+
isValidIdentityStub.returns(true);
|
|
457
|
+
expect(
|
|
458
|
+
strategy['isQueryValid'](
|
|
459
|
+
query.change({ 'openid.return_to': 'content' }),
|
|
460
|
+
),
|
|
461
|
+
).equal(false);
|
|
462
|
+
expect(isValidIdentityStub.callCount).equal(1);
|
|
463
|
+
expect(
|
|
464
|
+
isValidIdentityStub.calledWithExactly(
|
|
465
|
+
query.properties['openid.claimed_id'],
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe('isValidIdentity', () => {
|
|
472
|
+
it('Is valid identity', () => {
|
|
473
|
+
const identity = 'https://steamcommunity.com/openid/id/76561197960435530';
|
|
474
|
+
expect(strategy['isValidIdentity'](identity)).equal(true);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('Is not a string', () => {
|
|
478
|
+
const identity = null;
|
|
479
|
+
expect(strategy['isValidIdentity'](identity)).equal(false);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('Does not match regex', () => {
|
|
483
|
+
const identity =
|
|
484
|
+
'https://localhost:3000?x=https://steamcommunity.com/openid/id/76561197960435530';
|
|
485
|
+
expect(strategy['isValidIdentity'](identity)).equal(false);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe('validateAgainstSteam', () => {
|
|
490
|
+
let axiosPostStub: sinon.SinonStub;
|
|
491
|
+
let getOpenIdValidationRequestBodyStub: sinon.SinonStub;
|
|
492
|
+
let isSteamResponseValidStub: sinon.SinonStub;
|
|
493
|
+
|
|
494
|
+
const query = {};
|
|
495
|
+
const data = {};
|
|
496
|
+
const body = 'body';
|
|
497
|
+
|
|
498
|
+
beforeEach(() => {
|
|
499
|
+
axiosPostStub = sinon.stub(strategy['axios'], 'post');
|
|
500
|
+
getOpenIdValidationRequestBodyStub = sinon
|
|
501
|
+
.stub(strategy as any, 'getOpenIdValidationRequestBody')
|
|
502
|
+
.returns(body);
|
|
503
|
+
isSteamResponseValidStub = sinon.stub(
|
|
504
|
+
strategy as any,
|
|
505
|
+
'isSteamResponseValid',
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
afterEach(() => {
|
|
510
|
+
expect(getOpenIdValidationRequestBodyStub.callCount).equal(1);
|
|
511
|
+
expect(getOpenIdValidationRequestBodyStub.calledWithExactly(query)).equal(
|
|
512
|
+
true,
|
|
513
|
+
);
|
|
514
|
+
expect(axiosPostStub.callCount).equal(1);
|
|
515
|
+
expect(
|
|
516
|
+
axiosPostStub.calledWithExactly(VALID_OPENID_ENDPOINT, body, {
|
|
517
|
+
maxRedirects: 0,
|
|
518
|
+
headers: {
|
|
519
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
520
|
+
},
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('Validates successfully', async () => {
|
|
526
|
+
axiosPostStub.resolves({ status: 200, data });
|
|
527
|
+
isSteamResponseValidStub.returns(true);
|
|
528
|
+
|
|
529
|
+
expect(await strategy['validateAgainstSteam'](query as any)).equal(true);
|
|
530
|
+
expect(isSteamResponseValidStub.callCount).equal(1);
|
|
531
|
+
expect(isSteamResponseValidStub.calledWith(data)).equal(true);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('Wrong status code', async () => {
|
|
535
|
+
axiosPostStub.resolves({ status: 100, data });
|
|
536
|
+
|
|
537
|
+
expect(await strategy['validateAgainstSteam'](query as any)).equal(false);
|
|
538
|
+
expect(isSteamResponseValidStub.callCount).equal(0);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('Response is invalid', async () => {
|
|
542
|
+
axiosPostStub.resolves({ status: 200, data });
|
|
543
|
+
isSteamResponseValidStub.returns(false);
|
|
544
|
+
|
|
545
|
+
expect(await strategy['validateAgainstSteam'](query as any)).equal(false);
|
|
546
|
+
expect(isSteamResponseValidStub.callCount).equal(1);
|
|
547
|
+
expect(isSteamResponseValidStub.calledWith(data)).equal(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('Axios rejects', async () => {
|
|
551
|
+
axiosPostStub.rejects(new Error('Malformed.'));
|
|
552
|
+
|
|
553
|
+
expect(await strategy['validateAgainstSteam'](query as any)).equal(false);
|
|
554
|
+
expect(isSteamResponseValidStub.callCount).equal(0);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('getOpenIdValidationRequestBody', () => {
|
|
559
|
+
it('Stringifies query', () => {
|
|
560
|
+
const query = {
|
|
561
|
+
'openid.op_endpoint': VALID_OPENID_ENDPOINT,
|
|
562
|
+
'openid.mode': 'id_res',
|
|
563
|
+
};
|
|
564
|
+
expect(strategy['getOpenIdValidationRequestBody'](query as any)).equal(
|
|
565
|
+
qs.stringify({ ...query, 'openid.mode': 'check_authentication' }),
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('isSteamResponseValid', () => {
|
|
571
|
+
describe('Is valid steam response', () => {
|
|
572
|
+
const response = `ns:${VALID_NONCE}\nis_valid:true\n`;
|
|
573
|
+
expect(strategy['isSteamResponseValid'](response)).equal(true);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('Response is not a string', () => {
|
|
577
|
+
const response = {};
|
|
578
|
+
expect(strategy['isSteamResponseValid'](response)).equal(false);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('Response does not match', () => {
|
|
582
|
+
const response = `ns:${VALID_NONCE}\nis_valid:true`;
|
|
583
|
+
expect(strategy['isSteamResponseValid'](response)).equal(false);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe('Invalid nonce', () => {
|
|
587
|
+
const response = `ns:xddd\nis_valid:true\n`;
|
|
588
|
+
expect(strategy['isSteamResponseValid'](response)).equal(false);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe('Marked invalid', () => {
|
|
592
|
+
const response = `ns:${VALID_NONCE}\nis_valid:false\n`;
|
|
593
|
+
expect(strategy['isSteamResponseValid'](response)).equal(false);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
describe('No match', () => {
|
|
597
|
+
const response = `invalid`;
|
|
598
|
+
expect(strategy['isSteamResponseValid'](response)).equal(false);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
describe('getSteamId', () => {
|
|
603
|
+
it('Retrieves steamid from claimed_id property', () => {
|
|
604
|
+
const steamid = '76561197960435530';
|
|
605
|
+
const q = {
|
|
606
|
+
'openid.claimed_id': `https://steamcommunity.com/openid/id/${steamid}`,
|
|
607
|
+
};
|
|
608
|
+
expect(strategy['getSteamId'](q as any)).equal(steamid);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('getUser', () => {
|
|
613
|
+
let fetchPlayerSummaryStub: sinon.SinonStub;
|
|
614
|
+
|
|
615
|
+
beforeEach(() => {
|
|
616
|
+
fetchPlayerSummaryStub = sinon.stub(
|
|
617
|
+
strategy as any,
|
|
618
|
+
'fetchPlayerSummary',
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const steamid = '76561197960435530';
|
|
623
|
+
|
|
624
|
+
it('Gets user without profile', async () => {
|
|
625
|
+
expect(await strategy['getUser'](steamid)).deep.equal({ steamid });
|
|
626
|
+
expect(fetchPlayerSummaryStub.callCount).equal(0);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('Gets user with profile', async () => {
|
|
630
|
+
// @ts-expect-error
|
|
631
|
+
strategy['profile'] = true;
|
|
632
|
+
|
|
633
|
+
const profile = { steamid, avatar: 'url' };
|
|
634
|
+
fetchPlayerSummaryStub.resolves(profile);
|
|
635
|
+
|
|
636
|
+
expect(await strategy['getUser'](steamid)).equal(profile);
|
|
637
|
+
expect(fetchPlayerSummaryStub.callCount).equal(1);
|
|
638
|
+
expect(fetchPlayerSummaryStub.calledWithExactly(steamid)).equal(true);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe('fetchPlayerSummary', async () => {
|
|
643
|
+
let axiosGetStub: sinon.SinonStub;
|
|
644
|
+
|
|
645
|
+
const steamid = '76561197960435530';
|
|
646
|
+
const apiKey = 'xxx';
|
|
647
|
+
|
|
648
|
+
beforeEach(() => {
|
|
649
|
+
strategy = new SteamOpenIdStrategy({
|
|
650
|
+
apiKey,
|
|
651
|
+
profile: true,
|
|
652
|
+
returnURL: RETURN_URL,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
axiosGetStub = sinon.stub(strategy['axios'], 'get');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
afterEach(() => {
|
|
659
|
+
expect(axiosGetStub.callCount).equal(1);
|
|
660
|
+
expect(
|
|
661
|
+
axiosGetStub.calledWithExactly(
|
|
662
|
+
`${PLAYER_SUMMARY_URL}/?${qs.stringify({
|
|
663
|
+
steamids: steamid,
|
|
664
|
+
key: apiKey,
|
|
665
|
+
})}`,
|
|
666
|
+
),
|
|
667
|
+
).equal(true);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('Successfully fetches valid user profile', async () => {
|
|
671
|
+
const profile = { steamid };
|
|
672
|
+
axiosGetStub.resolves({ data: { response: { players: [profile] } } });
|
|
673
|
+
|
|
674
|
+
expect(await strategy['fetchPlayerSummary'](steamid)).equal(profile);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('Malformed response, players is not an array', async () => {
|
|
678
|
+
axiosGetStub.resolves({ data: { response: { players: null } } });
|
|
679
|
+
|
|
680
|
+
expect(strategy['fetchPlayerSummary'](steamid)).to.rejectedWith(Error);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('Malformed response, response is missing', async () => {
|
|
684
|
+
axiosGetStub.resolves({ data: {} });
|
|
685
|
+
|
|
686
|
+
expect(strategy['fetchPlayerSummary'](steamid)).to.rejectedWith(Error);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('Malformed response, data is missing', async () => {
|
|
690
|
+
axiosGetStub.resolves({});
|
|
691
|
+
|
|
692
|
+
expect(strategy['fetchPlayerSummary'](steamid)).to.rejectedWith(Error);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('No user returned', async () => {
|
|
696
|
+
axiosGetStub.resolves({ data: { response: { players: [] } } });
|
|
697
|
+
|
|
698
|
+
let err: any;
|
|
699
|
+
try {
|
|
700
|
+
await strategy['fetchPlayerSummary'](steamid);
|
|
701
|
+
} catch (e) {
|
|
702
|
+
err = e;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
expect(err).instanceOf(SteamOpenIdError);
|
|
706
|
+
expect(err).to.have.property('code', SteamOpenIdErrorType.InvalidSteamId);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('Wrong user returned', async () => {
|
|
710
|
+
axiosGetStub.resolves({
|
|
711
|
+
data: { response: { players: [{ steamid: 'x' }] } },
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
let err: any;
|
|
715
|
+
try {
|
|
716
|
+
await strategy['fetchPlayerSummary'](steamid);
|
|
717
|
+
} catch (e) {
|
|
718
|
+
err = e;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
expect(err).instanceOf(SteamOpenIdError);
|
|
722
|
+
expect(err).to.have.property('code', SteamOpenIdErrorType.InvalidSteamId);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
});
|
package/tsconfig.json
CHANGED