@metaplay/metaplay-auth 1.0.1 → 1.1.1
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/CHANGELOG.md +12 -0
- package/dist/auth.js +226 -35
- package/dist/auth.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/utils.js +0 -19
- package/dist/utils.js.map +1 -1
- package/package.json +15 -10
- package/src/auth.ts +250 -34
- package/src/index.ts +14 -15
- package/src/utils.ts +0 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.1] - 2024-02-23
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
* Bumped version for package re-publishing.
|
|
8
|
+
|
|
9
|
+
## [1.1.0] - 2024-02-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* Added support for inexplicit token refresh flow.
|
|
14
|
+
|
|
3
15
|
## [1.0.1] - 2024-02-08
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/dist/auth.js
CHANGED
|
@@ -2,18 +2,34 @@
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import * as crypto from 'crypto';
|
|
5
|
-
import
|
|
5
|
+
import jwt from 'jsonwebtoken';
|
|
6
|
+
import jwkToPem from 'jwk-to-pem';
|
|
7
|
+
import { Configuration, WellknownApi } from '@ory/client';
|
|
6
8
|
import { setSecret, getSecret, removeSecret } from './secret_store.js';
|
|
9
|
+
import { createServer } from 'net';
|
|
7
10
|
import { logger } from './logging.js';
|
|
8
11
|
// oauth2 client details (maybe move these to be discovered from some online location to make changes easier to manage?)
|
|
9
|
-
const clientId = '
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
+
const clientId = '1acc28dc-d1a4-447c-ba5d-7acbcfaae98d';
|
|
13
|
+
const baseURL = 'https://auth.metaplay.dev';
|
|
14
|
+
const authorizationEndpoint = `${baseURL}/oauth2/auth`;
|
|
15
|
+
const tokenEndpoint = `${baseURL}/oauth2/token`;
|
|
16
|
+
const wellknownApi = new WellknownApi(new Configuration({
|
|
17
|
+
basePath: baseURL,
|
|
18
|
+
}));
|
|
19
|
+
const tokenList = ['id_token', 'access_token', 'refresh_token']; // List of compulsory tokens
|
|
20
|
+
/**
|
|
21
|
+
* A helper function which generates a code verifier and challenge for exchaning code from Ory server.
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
12
24
|
function generateCodeVerifierAndChallenge() {
|
|
13
25
|
const verifier = crypto.randomBytes(32).toString('hex');
|
|
14
26
|
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
15
27
|
return { verifier, challenge };
|
|
16
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A helper function which finds an local available port to listen on.
|
|
31
|
+
* @returns A promise that resolves to an available port.
|
|
32
|
+
*/
|
|
17
33
|
async function findAvailablePort() {
|
|
18
34
|
return await new Promise((resolve, reject) => {
|
|
19
35
|
// Ports need to be in sync with oauth2 client callbacks.
|
|
@@ -43,6 +59,9 @@ async function findAvailablePort() {
|
|
|
43
59
|
tryNextPort();
|
|
44
60
|
});
|
|
45
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Log in and save tokens to the local secret store.
|
|
64
|
+
*/
|
|
46
65
|
export async function loginAndSaveTokens() {
|
|
47
66
|
// Find an available port to listen on.
|
|
48
67
|
const availablePort = await findAvailablePort();
|
|
@@ -56,64 +75,236 @@ export async function loginAndSaveTokens() {
|
|
|
56
75
|
const error = req.query.error;
|
|
57
76
|
const errorDescription = req.query.error_description;
|
|
58
77
|
if (error) {
|
|
59
|
-
console.
|
|
78
|
+
console.error(`Error logging in. Received the following error:\n\n${error}: ${errorDescription}`);
|
|
60
79
|
res.send(`Authentication failed: ${error}: ${errorDescription}`);
|
|
61
80
|
server.close();
|
|
62
81
|
process.exit(1);
|
|
63
82
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
try {
|
|
84
|
+
const code = req.query.code;
|
|
85
|
+
logger.debug(`Received callback request with code ${code}. Preparing to exchange for tokens...`);
|
|
86
|
+
const tokens = await getTokensWithAuthorizationCode(state, redirectUri, verifier, code);
|
|
87
|
+
// TODO: Could return a pre-generated HTML page instead of text?
|
|
88
|
+
res.send('Authentication successful! You can close this window.');
|
|
89
|
+
await saveTokens(tokens);
|
|
90
|
+
console.log('You are now logged in and can call the other commands.');
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
console.error(`Error: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
server.close();
|
|
99
|
+
}
|
|
75
100
|
process.exit(0);
|
|
76
101
|
});
|
|
77
102
|
// Start the server.
|
|
78
103
|
const server = app.listen(availablePort, () => {
|
|
79
|
-
const authorizationUrl = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=openid&state=${encodeURIComponent(state)}`;
|
|
104
|
+
const authorizationUrl = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=openid offline_access&state=${encodeURIComponent(state)}`;
|
|
80
105
|
console.log(`Attempting to open a browser to log in. If a browser did not open up, you can copy-paste the following URL to authenticate:\n\n${authorizationUrl}\n`);
|
|
81
106
|
void open(authorizationUrl);
|
|
82
107
|
logger.debug(`Listening on port ${availablePort} and waiting for callback...`);
|
|
83
108
|
});
|
|
84
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Refresh access and ID token with a refresh token.
|
|
112
|
+
*/
|
|
113
|
+
export async function extendCurrentSession() {
|
|
114
|
+
try {
|
|
115
|
+
const tokens = await loadTokens();
|
|
116
|
+
logger.debug('Validating access token...');
|
|
117
|
+
if (await validateToken(tokens.access_token)) {
|
|
118
|
+
// Access token is not expired, return to the caller
|
|
119
|
+
logger.debug('Access token is valid, return to the caller.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
logger.debug('Access token is no longer valid, trying to extend the current session with a refresh token.');
|
|
123
|
+
const refreshedTokens = await extendCurrentSessionWithRefreshToken(tokens.refresh_token);
|
|
124
|
+
await saveTokens(refreshedTokens);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof Error) {
|
|
128
|
+
console.error(error.message);
|
|
129
|
+
}
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Make HTTP request to the token endpoint for a new set of tokens using the refresh token.
|
|
135
|
+
* @param refreshToken: The refresh token to use to get a new set of tokens.
|
|
136
|
+
* @returns A promise that resolves to a new set of tokens.
|
|
137
|
+
*/
|
|
138
|
+
async function extendCurrentSessionWithRefreshToken(refreshToken) {
|
|
139
|
+
const params = new URLSearchParams({
|
|
140
|
+
grant_type: 'refresh_token',
|
|
141
|
+
refresh_token: refreshToken,
|
|
142
|
+
scope: 'openid offline_access',
|
|
143
|
+
client_id: clientId
|
|
144
|
+
});
|
|
145
|
+
logger.debug('Refreshing tokens...');
|
|
146
|
+
// Send a POST request to refresh tokens
|
|
147
|
+
const response = await fetch(tokenEndpoint, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
151
|
+
},
|
|
152
|
+
body: params.toString(),
|
|
153
|
+
});
|
|
154
|
+
// Check if the response is OK
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const responseJSON = await response.json();
|
|
157
|
+
logger.error('Failed to refresh tokens.');
|
|
158
|
+
logger.error(`Error Type: ${responseJSON.error}`);
|
|
159
|
+
logger.error(`Error Description: ${responseJSON.error_description}`);
|
|
160
|
+
logger.debug('Attempt to clear local state and remove any dangling tokens...');
|
|
161
|
+
await removeTokens();
|
|
162
|
+
logger.debug('Local state cleared and tokens removed.');
|
|
163
|
+
throw new Error('Failed extending current session, exiting. Please log in again.');
|
|
164
|
+
}
|
|
165
|
+
return await response.json();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Make HTTP request to the token endpoint for a new set of tokens (Authorization Code Flow).
|
|
169
|
+
* @param state
|
|
170
|
+
* @param redirectUri
|
|
171
|
+
* @param verifier
|
|
172
|
+
* @param code
|
|
173
|
+
* @returns
|
|
174
|
+
*/
|
|
175
|
+
async function getTokensWithAuthorizationCode(state, redirectUri, verifier, code) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(tokenEndpoint, {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: {
|
|
180
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
181
|
+
},
|
|
182
|
+
body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`
|
|
183
|
+
});
|
|
184
|
+
return await response.json();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (error instanceof Error) {
|
|
188
|
+
logger.error(`Error exchanging code for tokens: ${error.message}`);
|
|
189
|
+
}
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Load tokens from the local secret store.
|
|
195
|
+
*/
|
|
85
196
|
export async function loadTokens() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
197
|
+
try {
|
|
198
|
+
const idToken = await getSecret('id_token');
|
|
199
|
+
const accessToken = await getSecret('access_token');
|
|
200
|
+
const refreshToken = await getSecret('refresh_token');
|
|
201
|
+
if (idToken == null || accessToken == null || refreshToken == null) {
|
|
202
|
+
throw new Error('Metaplay tokens not found. Are you logged in?');
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
id_token: idToken,
|
|
206
|
+
access_token: accessToken,
|
|
207
|
+
refresh_token: refreshToken
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof Error) {
|
|
212
|
+
throw new Error(`Error loading tokens: ${error.message}`);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Save tokens to the local secret store.
|
|
219
|
+
* @param tokens The tokens to save.
|
|
220
|
+
*/
|
|
221
|
+
export async function saveTokens(tokens) {
|
|
222
|
+
try {
|
|
223
|
+
logger.debug('Received new tokens, verifying...');
|
|
224
|
+
// Check if all tokens are present
|
|
225
|
+
for (const tokenName of tokenList) {
|
|
226
|
+
if (tokens[tokenName] === undefined) {
|
|
227
|
+
throw new Error(`Metaplay token ${tokenName} not found. Please log in again and make sure all checkboxes of permissions are selected before proceeding.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
logger.debug('Token verification completed, storing tokens...');
|
|
231
|
+
await setSecret('id_token', tokens.id_token);
|
|
232
|
+
await setSecret('access_token', tokens.access_token);
|
|
233
|
+
await setSecret('refresh_token', tokens.refresh_token);
|
|
234
|
+
logger.debug('Tokens successfully stored.');
|
|
235
|
+
await showTokenInfo(tokens.access_token);
|
|
236
|
+
// await showTokenInfo(tokens.id_token)
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (error instanceof Error) {
|
|
240
|
+
throw new Error(`Failed to save tokens: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
90
243
|
}
|
|
91
|
-
return {
|
|
92
|
-
id_token: idToken,
|
|
93
|
-
access_token: accessToken
|
|
94
|
-
};
|
|
95
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Remove tokens from the local secret store.
|
|
247
|
+
*/
|
|
96
248
|
export async function removeTokens() {
|
|
97
249
|
try {
|
|
98
250
|
await removeSecret('id_token');
|
|
99
251
|
logger.debug('Removed id_token.');
|
|
100
252
|
await removeSecret('access_token');
|
|
101
253
|
logger.debug('Removed access_token.');
|
|
254
|
+
await removeSecret('refresh_token');
|
|
255
|
+
logger.debug('Removed refresh_token.');
|
|
102
256
|
}
|
|
103
257
|
catch (error) {
|
|
104
258
|
if (error instanceof Error) {
|
|
105
|
-
throw new Error(`
|
|
259
|
+
throw new Error(`Error removing tokens: ${error.message}`);
|
|
106
260
|
}
|
|
261
|
+
throw error;
|
|
107
262
|
}
|
|
108
263
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
264
|
+
/**
|
|
265
|
+
* This function validates a token by checking its signature, expiration and issuer.
|
|
266
|
+
* @param token The token being validated
|
|
267
|
+
* @returns return if token is valid, throw errors otherwise
|
|
268
|
+
*/
|
|
269
|
+
async function validateToken(token) {
|
|
270
|
+
try {
|
|
271
|
+
// Decode the token
|
|
272
|
+
const completeTokenData = jwt.decode(token, { complete: true });
|
|
273
|
+
// Handle junk data.
|
|
274
|
+
if (!completeTokenData) {
|
|
275
|
+
throw new Error('Invalid token');
|
|
276
|
+
}
|
|
277
|
+
// Get the JSON web keys from the wellknown API.
|
|
278
|
+
const res = await wellknownApi.discoverJsonWebKeys();
|
|
279
|
+
const jwk = res.data.keys?.find((key) => key.kid === completeTokenData.header.kid);
|
|
280
|
+
if (!jwk) {
|
|
281
|
+
throw new Error('No public key found for token signature');
|
|
282
|
+
}
|
|
283
|
+
// Convert to PEM format.
|
|
284
|
+
const pem = jwkToPem(jwk);
|
|
285
|
+
// Verify the token signature, expiration and decode it.
|
|
286
|
+
jwt.verify(token, pem, { issuer: baseURL, ignoreExpiration: false });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof Error) {
|
|
291
|
+
logger.info(`Token not valid: ${error.message}`);
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* A helper function which shows token info after being fully decoded.
|
|
298
|
+
* @param token The token to show info for.
|
|
299
|
+
*/
|
|
300
|
+
async function showTokenInfo(token) {
|
|
301
|
+
logger.debug('Showing access token info...');
|
|
302
|
+
// Decode the token
|
|
303
|
+
const completeTokenData = jwt.decode(token, { complete: true });
|
|
304
|
+
// Handle junk data.
|
|
305
|
+
if (!completeTokenData) {
|
|
306
|
+
throw new Error('Token is corrupted');
|
|
307
|
+
}
|
|
308
|
+
logger.debug(JSON.stringify(completeTokenData));
|
|
118
309
|
}
|
|
119
310
|
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { loginAndSaveTokens, loadTokens, removeTokens } from './auth.js';
|
|
3
|
+
import { loginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './auth.js';
|
|
4
4
|
import { StackAPI } from './stackapi.js';
|
|
5
|
-
import { validateTokens } from './utils.js';
|
|
6
5
|
import { setLogLevel } from './logging.js';
|
|
7
6
|
import { exit } from 'process';
|
|
8
7
|
const program = new Command();
|
|
@@ -44,6 +43,9 @@ program.command('logout')
|
|
|
44
43
|
});
|
|
45
44
|
program.command('show-tokens')
|
|
46
45
|
.description('show loaded tokens')
|
|
46
|
+
.hook('preAction', async () => {
|
|
47
|
+
await extendCurrentSession();
|
|
48
|
+
})
|
|
47
49
|
.action(async () => {
|
|
48
50
|
try {
|
|
49
51
|
// TODO: Could detect if not logged in and fail more gracefully?
|
|
@@ -64,12 +66,12 @@ program.command('get-kubeconfig')
|
|
|
64
66
|
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
65
67
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
66
68
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
69
|
+
.hook('preAction', async () => {
|
|
70
|
+
await extendCurrentSession();
|
|
71
|
+
})
|
|
67
72
|
.action(async (gameserver, options) => {
|
|
68
73
|
try {
|
|
69
74
|
const tokens = await loadTokens();
|
|
70
|
-
if (!validateTokens(tokens)) {
|
|
71
|
-
throw new Error('Invalid tokens; try to refresh?');
|
|
72
|
-
}
|
|
73
75
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
74
76
|
throw new Error('Could not determine a deployment to fetch the KubeConfigs from. You need to specify either a gameserver or an organization, project, and environment');
|
|
75
77
|
}
|
|
@@ -96,15 +98,15 @@ program.command('get-aws-credentials')
|
|
|
96
98
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
97
99
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
98
100
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
101
|
+
.hook('preAction', async () => {
|
|
102
|
+
await extendCurrentSession();
|
|
103
|
+
})
|
|
99
104
|
.action(async (gameserver, options) => {
|
|
100
105
|
try {
|
|
101
106
|
if (options.format !== 'json' && options.format !== 'env') {
|
|
102
107
|
throw new Error('Invalid format; must be one of json or env');
|
|
103
108
|
}
|
|
104
109
|
const tokens = await loadTokens();
|
|
105
|
-
if (!validateTokens(tokens)) {
|
|
106
|
-
throw new Error('Invalid tokens; try to refresh?');
|
|
107
|
-
}
|
|
108
110
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
109
111
|
throw new Error('Could not determine a deployment to fetch the AWS credentials from. You need to specify either a gameserver or an organization, project, and environment');
|
|
110
112
|
}
|
|
@@ -140,12 +142,12 @@ program.command('get-environment')
|
|
|
140
142
|
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
141
143
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
142
144
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
145
|
+
.hook('preAction', async () => {
|
|
146
|
+
await extendCurrentSession();
|
|
147
|
+
})
|
|
143
148
|
.action(async (gameserver, options) => {
|
|
144
149
|
try {
|
|
145
150
|
const tokens = await loadTokens();
|
|
146
|
-
if (!validateTokens(tokens)) {
|
|
147
|
-
throw new Error('Invalid tokens; try to refresh?');
|
|
148
|
-
}
|
|
149
151
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
150
152
|
throw new Error('Could not determine a deployment to fetch environment details from. You need to specify either a gameserver or an organization, project, and environment');
|
|
151
153
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.js
CHANGED
|
@@ -38,23 +38,4 @@ export function getGameserverAdminUrl(uri) {
|
|
|
38
38
|
const domain = uri.substring(hostname.length + 1);
|
|
39
39
|
return `${hostname}-admin.${domain}`;
|
|
40
40
|
}
|
|
41
|
-
export function validateTokens(tokens) {
|
|
42
|
-
if (tokens.id_token == null) {
|
|
43
|
-
throw new Error('id_token is missing');
|
|
44
|
-
}
|
|
45
|
-
if (tokens.access_token == null) {
|
|
46
|
-
throw new Error('access_token is missing');
|
|
47
|
-
}
|
|
48
|
-
if (!isJwtTokenExpired(tokens.id_token)) {
|
|
49
|
-
throw new Error('id_token is expired');
|
|
50
|
-
}
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
function isJwtTokenExpired(token) {
|
|
54
|
-
const base64Url = token.split('.')[1];
|
|
55
|
-
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
56
|
-
const payload = JSON.parse(Buffer.from(base64, 'base64').toString());
|
|
57
|
-
const now = Math.floor(Date.now() / 1000);
|
|
58
|
-
return payload.exp > now;
|
|
59
|
-
}
|
|
60
41
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,uBAAuB,CAAE,GAAW,EAAE,OAA4B;IAChF,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAE,MAAc;IACzC,MAAM,SAAS,GAAG,+EAA+E,CAAA;IAEjG,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAE,SAAiB;IACnD,IAAI;QACF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QAE9B,oEAAoE;QACpE,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA,CAAC,2BAA2B;QACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,IAAI,CAAA;QAE7B,yDAAyD;QACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;QAE9C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;KAC5C;IAAC,OAAO,KAAK,EAAE;QACd,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAA;KAC/B;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAE,GAAW;IAChD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAE5B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAEjD,OAAO,GAAG,QAAQ,UAAU,MAAM,EAAE,CAAA;AACtC,CAAC
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,uBAAuB,CAAE,GAAW,EAAE,OAA4B;IAChF,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAE,MAAc;IACzC,MAAM,SAAS,GAAG,+EAA+E,CAAA;IAEjG,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAE,SAAiB;IACnD,IAAI;QACF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;QAE9B,oEAAoE;QACpE,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA,CAAC,2BAA2B;QACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,IAAI,CAAA;QAE7B,yDAAyD;QACzD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;QAE9C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;KAC5C;IAAC,OAAO,KAAK,EAAE;QACd,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAA;KAC/B;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAE,GAAW;IAChD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAE5B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAEjD,OAAO,GAAG,QAAQ,UAAU,MAAM,EAAE,CAAA;AACtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaplay/metaplay-auth",
|
|
3
3
|
"description": "Utility CLI for authenticating with the Metaplay Auth and making authenticated calls to infrastructure endpoints.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"homepage": "https://metaplay.io",
|
|
8
8
|
"bin": "dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsx src/index.ts",
|
|
11
|
+
"prepublish": "tsc"
|
|
12
|
+
},
|
|
9
13
|
"publishConfig": {
|
|
10
14
|
"access": "public"
|
|
11
15
|
},
|
|
12
16
|
"devDependencies": {
|
|
17
|
+
"@metaplay/eslint-config": "workspace:*",
|
|
18
|
+
"@metaplay/typescript-config": "workspace:*",
|
|
19
|
+
"@ory/client": "^1.4.5",
|
|
13
20
|
"@types/express": "^4.17.21",
|
|
14
|
-
"@types/
|
|
21
|
+
"@types/jsonwebtoken": "^9.0.5",
|
|
22
|
+
"@types/jwk-to-pem": "^2.0.3",
|
|
23
|
+
"@types/node": "^20.11.19",
|
|
24
|
+
"jsonwebtoken": "^9.0.2",
|
|
25
|
+
"jwk-to-pem": "^2.0.5",
|
|
15
26
|
"typescript": "^5.1.6",
|
|
16
|
-
"tsx": "^4.7.0"
|
|
17
|
-
"@metaplay/typescript-config": "1.0.0",
|
|
18
|
-
"@metaplay/eslint-config": "1.0.0"
|
|
27
|
+
"tsx": "^4.7.0"
|
|
19
28
|
},
|
|
20
29
|
"dependencies": {
|
|
21
30
|
"commander": "^11.1.0",
|
|
@@ -25,9 +34,5 @@
|
|
|
25
34
|
"open": "^10.0.2",
|
|
26
35
|
"process": "^0.11.10",
|
|
27
36
|
"tslog": "^4.9.2"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"dev": "tsx src/index.ts",
|
|
31
|
-
"prepublish": "tsc"
|
|
32
37
|
}
|
|
33
|
-
}
|
|
38
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -2,21 +2,37 @@
|
|
|
2
2
|
import express from 'express'
|
|
3
3
|
import open from 'open'
|
|
4
4
|
import * as crypto from 'crypto'
|
|
5
|
-
import
|
|
5
|
+
import jwt from 'jsonwebtoken'
|
|
6
|
+
import jwkToPem from 'jwk-to-pem'
|
|
7
|
+
import { Configuration, WellknownApi } from '@ory/client'
|
|
6
8
|
import { setSecret, getSecret, removeSecret } from './secret_store.js'
|
|
9
|
+
import { createServer } from 'net'
|
|
7
10
|
import { logger } from './logging.js'
|
|
8
11
|
|
|
9
12
|
// oauth2 client details (maybe move these to be discovered from some online location to make changes easier to manage?)
|
|
10
13
|
const clientId = 'c16ea663-ced3-46c6-8f85-38c9681fe1f0'
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const baseURL = 'https://auth.metaplay.dev'
|
|
15
|
+
const authorizationEndpoint = `${baseURL}/oauth2/auth`
|
|
16
|
+
const tokenEndpoint = `${baseURL}/oauth2/token`
|
|
17
|
+
const wellknownApi = new WellknownApi(new Configuration({
|
|
18
|
+
basePath: baseURL,
|
|
19
|
+
}))
|
|
20
|
+
const tokenList: string[] = ['id_token', 'access_token', 'refresh_token'] // List of compulsory tokens
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A helper function which generates a code verifier and challenge for exchaning code from Ory server.
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
14
26
|
function generateCodeVerifierAndChallenge (): { verifier: string, challenge: string } {
|
|
15
27
|
const verifier: string = crypto.randomBytes(32).toString('hex')
|
|
16
28
|
const challenge: string = crypto.createHash('sha256').update(verifier).digest('base64url')
|
|
17
29
|
return { verifier, challenge }
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A helper function which finds an local available port to listen on.
|
|
34
|
+
* @returns A promise that resolves to an available port.
|
|
35
|
+
*/
|
|
20
36
|
async function findAvailablePort (): Promise<number> {
|
|
21
37
|
return await new Promise((resolve, reject) => {
|
|
22
38
|
// Ports need to be in sync with oauth2 client callbacks.
|
|
@@ -51,6 +67,9 @@ async function findAvailablePort (): Promise<number> {
|
|
|
51
67
|
})
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Log in and save tokens to the local secret store.
|
|
72
|
+
*/
|
|
54
73
|
export async function loginAndSaveTokens () {
|
|
55
74
|
// Find an available port to listen on.
|
|
56
75
|
const availablePort = await findAvailablePort()
|
|
@@ -67,32 +86,37 @@ export async function loginAndSaveTokens () {
|
|
|
67
86
|
const errorDescription: string | undefined = req.query.error_description as string
|
|
68
87
|
|
|
69
88
|
if (error) {
|
|
70
|
-
console.
|
|
89
|
+
console.error(`Error logging in. Received the following error:\n\n${error}: ${errorDescription}`)
|
|
71
90
|
res.send(`Authentication failed: ${error}: ${errorDescription}`)
|
|
72
91
|
server.close()
|
|
73
92
|
process.exit(1)
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
try {
|
|
96
|
+
const code: string | undefined = req.query.code as string
|
|
97
|
+
logger.debug(`Received callback request with code ${code}. Preparing to exchange for tokens...`)
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
const tokens = await getTokensWithAuthorizationCode(state, redirectUri, verifier, code)
|
|
100
|
+
// TODO: Could return a pre-generated HTML page instead of text?
|
|
101
|
+
res.send('Authentication successful! You can close this window.')
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
await saveTokens(tokens)
|
|
104
|
+
|
|
105
|
+
console.log('You are now logged in and can call the other commands.')
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
console.error(`Error: ${error.message}`)
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
server.close()
|
|
112
|
+
}
|
|
84
113
|
|
|
85
|
-
console.log('Login tokens stored successfully.')
|
|
86
|
-
// TODO: Could return a pre-generated HTML page instead of text?
|
|
87
|
-
res.send('Authentication successful! You can close this window.')
|
|
88
|
-
server.close()
|
|
89
|
-
console.log('You are now logged in and can call the other commands.')
|
|
90
114
|
process.exit(0)
|
|
91
115
|
})
|
|
92
116
|
|
|
93
117
|
// Start the server.
|
|
94
118
|
const server = app.listen(availablePort, () => {
|
|
95
|
-
const authorizationUrl: string = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=openid&state=${encodeURIComponent(state)}`
|
|
119
|
+
const authorizationUrl: string = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=openid offline_access&state=${encodeURIComponent(state)}`
|
|
96
120
|
console.log(`Attempting to open a browser to log in. If a browser did not open up, you can copy-paste the following URL to authenticate:\n\n${authorizationUrl}\n`)
|
|
97
121
|
void open(authorizationUrl)
|
|
98
122
|
|
|
@@ -100,40 +124,232 @@ export async function loginAndSaveTokens () {
|
|
|
100
124
|
})
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Refresh access and ID token with a refresh token.
|
|
129
|
+
*/
|
|
130
|
+
export async function extendCurrentSession (): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
const tokens = await loadTokens()
|
|
133
|
+
|
|
134
|
+
logger.debug('Validating access token...')
|
|
135
|
+
if (await validateToken(tokens.access_token)) {
|
|
136
|
+
// Access token is not expired, return to the caller
|
|
137
|
+
logger.debug('Access token is valid, return to the caller.')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
logger.debug('Access token is no longer valid, trying to extend the current session with a refresh token.')
|
|
106
142
|
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
const refreshedTokens = await extendCurrentSessionWithRefreshToken(tokens.refresh_token)
|
|
144
|
+
|
|
145
|
+
await saveTokens(refreshedTokens)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof Error) {
|
|
148
|
+
console.error(error.message)
|
|
149
|
+
}
|
|
150
|
+
process.exit(1)
|
|
109
151
|
}
|
|
152
|
+
}
|
|
110
153
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Make HTTP request to the token endpoint for a new set of tokens using the refresh token.
|
|
156
|
+
* @param refreshToken: The refresh token to use to get a new set of tokens.
|
|
157
|
+
* @returns A promise that resolves to a new set of tokens.
|
|
158
|
+
*/
|
|
159
|
+
async function extendCurrentSessionWithRefreshToken (refreshToken: string): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
|
|
160
|
+
// TODO: similiar to the todo task in getTokensWithAuthorizationCode, http request can be handled by ory/client.
|
|
161
|
+
const params = new URLSearchParams({
|
|
162
|
+
grant_type: 'refresh_token',
|
|
163
|
+
refresh_token: refreshToken,
|
|
164
|
+
scope: 'openid offline_access',
|
|
165
|
+
client_id: clientId
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
logger.debug('Refreshing tokens...')
|
|
169
|
+
|
|
170
|
+
// Send a POST request to refresh tokens
|
|
171
|
+
const response = await fetch(tokenEndpoint, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
175
|
+
},
|
|
176
|
+
body: params.toString(),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Check if the response is OK
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const responseJSON = await response.json()
|
|
182
|
+
|
|
183
|
+
logger.error('Failed to refresh tokens.')
|
|
184
|
+
logger.error(`Error Type: ${responseJSON.error}`)
|
|
185
|
+
logger.error(`Error Description: ${responseJSON.error_description}`)
|
|
186
|
+
|
|
187
|
+
logger.debug('Attempt to clear local state and remove any dangling tokens...')
|
|
188
|
+
await removeTokens()
|
|
189
|
+
logger.debug('Local state cleared and tokens removed.')
|
|
190
|
+
|
|
191
|
+
throw new Error('Failed extending current session, exiting. Please log in again.')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return await response.json()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Make HTTP request to the token endpoint for a new set of tokens (Authorization Code Flow).
|
|
199
|
+
* @param state
|
|
200
|
+
* @param redirectUri
|
|
201
|
+
* @param verifier
|
|
202
|
+
* @param code
|
|
203
|
+
* @returns
|
|
204
|
+
*/
|
|
205
|
+
async function getTokensWithAuthorizationCode (state: string, redirectUri: string, verifier: string, code: string): Promise<{ id_token: string, access_token: string, refresh_token: string } | {}> {
|
|
206
|
+
// TODO: the authorication code exchange flow might be better to be handled by ory/client, could check if there's any useful toosl there.
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch(tokenEndpoint, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: {
|
|
211
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
212
|
+
},
|
|
213
|
+
body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return await response.json()
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof Error) {
|
|
219
|
+
logger.error(`Error exchanging code for tokens: ${error.message}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {}
|
|
114
223
|
}
|
|
115
224
|
}
|
|
116
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Load tokens from the local secret store.
|
|
228
|
+
*/
|
|
229
|
+
export async function loadTokens (): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
|
|
230
|
+
try {
|
|
231
|
+
const idToken = await getSecret('id_token')
|
|
232
|
+
const accessToken = await getSecret('access_token')
|
|
233
|
+
const refreshToken = await getSecret('refresh_token')
|
|
234
|
+
|
|
235
|
+
if (idToken == null || accessToken == null || refreshToken == null) {
|
|
236
|
+
throw new Error('Metaplay tokens not found. Are you logged in?')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
id_token: idToken,
|
|
241
|
+
access_token: accessToken,
|
|
242
|
+
refresh_token: refreshToken
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error instanceof Error) {
|
|
246
|
+
throw new Error(`Error loading tokens: ${error.message}`)
|
|
247
|
+
}
|
|
248
|
+
throw error
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Save tokens to the local secret store.
|
|
254
|
+
* @param tokens The tokens to save.
|
|
255
|
+
*/
|
|
256
|
+
export async function saveTokens (tokens: Record<string, string>): Promise<void> {
|
|
257
|
+
try {
|
|
258
|
+
logger.debug('Received new tokens, verifying...')
|
|
259
|
+
|
|
260
|
+
// Check if all tokens are present
|
|
261
|
+
for (const tokenName of tokenList) {
|
|
262
|
+
if (tokens[tokenName] === undefined) {
|
|
263
|
+
throw new Error(`Metaplay token ${tokenName} not found. Please log in again and make sure all checkboxes of permissions are selected before proceeding.`)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.debug('Token verification completed, storing tokens...')
|
|
268
|
+
|
|
269
|
+
await setSecret('id_token', tokens.id_token)
|
|
270
|
+
await setSecret('access_token', tokens.access_token)
|
|
271
|
+
await setSecret('refresh_token', tokens.refresh_token)
|
|
272
|
+
|
|
273
|
+
logger.debug('Tokens successfully stored.')
|
|
274
|
+
|
|
275
|
+
await showTokenInfo(tokens.access_token)
|
|
276
|
+
// await showTokenInfo(tokens.id_token)
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error instanceof Error) {
|
|
279
|
+
throw new Error(`Failed to save tokens: ${error.message}`)
|
|
280
|
+
}
|
|
281
|
+
throw error
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Remove tokens from the local secret store.
|
|
287
|
+
*/
|
|
117
288
|
export async function removeTokens (): Promise<void> {
|
|
118
289
|
try {
|
|
119
290
|
await removeSecret('id_token')
|
|
120
291
|
logger.debug('Removed id_token.')
|
|
121
292
|
await removeSecret('access_token')
|
|
122
293
|
logger.debug('Removed access_token.')
|
|
294
|
+
await removeSecret('refresh_token')
|
|
295
|
+
logger.debug('Removed refresh_token.')
|
|
123
296
|
} catch (error) {
|
|
124
297
|
if (error instanceof Error) {
|
|
125
|
-
throw new Error(`
|
|
298
|
+
throw new Error(`Error removing tokens: ${error.message}`)
|
|
126
299
|
}
|
|
300
|
+
throw error
|
|
127
301
|
}
|
|
128
302
|
}
|
|
129
303
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
304
|
+
/**
|
|
305
|
+
* This function validates a token by checking its signature, expiration and issuer.
|
|
306
|
+
* @param token The token being validated
|
|
307
|
+
* @returns return if token is valid, throw errors otherwise
|
|
308
|
+
*/
|
|
309
|
+
async function validateToken (token: string): Promise<boolean> {
|
|
310
|
+
try {
|
|
311
|
+
// Decode the token
|
|
312
|
+
const completeTokenData = jwt.decode(token, { complete: true })
|
|
313
|
+
|
|
314
|
+
// Handle junk data.
|
|
315
|
+
if (!completeTokenData) {
|
|
316
|
+
throw new Error('Invalid token')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Get the JSON web keys from the wellknown API.
|
|
320
|
+
const res = await wellknownApi.discoverJsonWebKeys()
|
|
321
|
+
const jwk = res.data.keys?.find((key) => key.kid === completeTokenData.header.kid)
|
|
322
|
+
if (!jwk) {
|
|
323
|
+
throw new Error('No public key found for token signature')
|
|
324
|
+
}
|
|
325
|
+
// Convert to PEM format.
|
|
326
|
+
const pem = jwkToPem(jwk as jwkToPem.JWK)
|
|
327
|
+
|
|
328
|
+
// Verify the token signature, expiration and decode it.
|
|
329
|
+
jwt.verify(token, pem, { issuer: baseURL, ignoreExpiration: false })
|
|
330
|
+
|
|
331
|
+
return true
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error instanceof Error) {
|
|
334
|
+
logger.info(`Token not valid: ${error.message}`)
|
|
335
|
+
}
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A helper function which shows token info after being fully decoded.
|
|
342
|
+
* @param token The token to show info for.
|
|
343
|
+
*/
|
|
344
|
+
async function showTokenInfo (token: string): Promise<void> {
|
|
345
|
+
logger.debug('Showing access token info...')
|
|
346
|
+
// Decode the token
|
|
347
|
+
const completeTokenData = jwt.decode(token, { complete: true })
|
|
348
|
+
|
|
349
|
+
// Handle junk data.
|
|
350
|
+
if (!completeTokenData) {
|
|
351
|
+
throw new Error('Token is corrupted')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
logger.debug(JSON.stringify(completeTokenData))
|
|
139
355
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander'
|
|
3
|
-
import { loginAndSaveTokens, loadTokens, removeTokens } from './auth.js'
|
|
3
|
+
import { loginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './auth.js'
|
|
4
4
|
import { StackAPI } from './stackapi.js'
|
|
5
|
-
import { validateTokens } from './utils.js'
|
|
6
5
|
import { setLogLevel } from './logging.js'
|
|
7
6
|
import { exit } from 'process'
|
|
8
7
|
|
|
@@ -11,7 +10,7 @@ const program = new Command()
|
|
|
11
10
|
program
|
|
12
11
|
.name('metaplay-auth')
|
|
13
12
|
.description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
|
|
14
|
-
.version('1.
|
|
13
|
+
.version('1.1.1')
|
|
15
14
|
.option('-d, --debug', 'enable debug output')
|
|
16
15
|
.hook('preAction', (thisCommand) => {
|
|
17
16
|
// Handle debug flag for all commands.
|
|
@@ -48,6 +47,9 @@ program.command('logout')
|
|
|
48
47
|
|
|
49
48
|
program.command('show-tokens')
|
|
50
49
|
.description('show loaded tokens')
|
|
50
|
+
.hook('preAction', async () => {
|
|
51
|
+
await extendCurrentSession()
|
|
52
|
+
})
|
|
51
53
|
.action(async () => {
|
|
52
54
|
try {
|
|
53
55
|
// TODO: Could detect if not logged in and fail more gracefully?
|
|
@@ -68,14 +70,13 @@ program.command('get-kubeconfig')
|
|
|
68
70
|
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
69
71
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
70
72
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
73
|
+
.hook('preAction', async () => {
|
|
74
|
+
await extendCurrentSession()
|
|
75
|
+
})
|
|
71
76
|
.action(async (gameserver, options) => {
|
|
72
77
|
try {
|
|
73
78
|
const tokens = await loadTokens()
|
|
74
79
|
|
|
75
|
-
if (!validateTokens(tokens)) {
|
|
76
|
-
throw new Error('Invalid tokens; try to refresh?')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
80
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
80
81
|
throw new Error('Could not determine a deployment to fetch the KubeConfigs from. You need to specify either a gameserver or an organization, project, and environment')
|
|
81
82
|
}
|
|
@@ -105,6 +106,9 @@ program.command('get-aws-credentials')
|
|
|
105
106
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
106
107
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
107
108
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
109
|
+
.hook('preAction', async () => {
|
|
110
|
+
await extendCurrentSession()
|
|
111
|
+
})
|
|
108
112
|
.action(async (gameserver, options) => {
|
|
109
113
|
try {
|
|
110
114
|
if (options.format !== 'json' && options.format !== 'env') {
|
|
@@ -113,10 +117,6 @@ program.command('get-aws-credentials')
|
|
|
113
117
|
|
|
114
118
|
const tokens = await loadTokens()
|
|
115
119
|
|
|
116
|
-
if (!validateTokens(tokens)) {
|
|
117
|
-
throw new Error('Invalid tokens; try to refresh?')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
120
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
121
121
|
throw new Error('Could not determine a deployment to fetch the AWS credentials from. You need to specify either a gameserver or an organization, project, and environment')
|
|
122
122
|
}
|
|
@@ -155,14 +155,13 @@ program.command('get-environment')
|
|
|
155
155
|
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
156
156
|
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
157
157
|
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
158
|
+
.hook('preAction', async () => {
|
|
159
|
+
await extendCurrentSession()
|
|
160
|
+
})
|
|
158
161
|
.action(async (gameserver, options) => {
|
|
159
162
|
try {
|
|
160
163
|
const tokens = await loadTokens()
|
|
161
164
|
|
|
162
|
-
if (!validateTokens(tokens)) {
|
|
163
|
-
throw new Error('Invalid tokens; try to refresh?')
|
|
164
|
-
}
|
|
165
|
-
|
|
166
165
|
if (!gameserver && !(options.organization && options.project && options.environment)) {
|
|
167
166
|
throw new Error('Could not determine a deployment to fetch environment details from. You need to specify either a gameserver or an organization, project, and environment')
|
|
168
167
|
}
|
package/src/utils.ts
CHANGED
|
@@ -46,27 +46,3 @@ export function getGameserverAdminUrl (uri: string): string {
|
|
|
46
46
|
|
|
47
47
|
return `${hostname}-admin.${domain}`
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
export function validateTokens (tokens: Record<string, string | null>): boolean {
|
|
51
|
-
if (tokens.id_token == null) {
|
|
52
|
-
throw new Error('id_token is missing')
|
|
53
|
-
}
|
|
54
|
-
if (tokens.access_token == null) {
|
|
55
|
-
throw new Error('access_token is missing')
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!isJwtTokenExpired(tokens.id_token)) {
|
|
59
|
-
throw new Error('id_token is expired')
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isJwtTokenExpired (token: string): boolean {
|
|
66
|
-
const base64Url = token.split('.')[1]
|
|
67
|
-
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
68
|
-
const payload = JSON.parse(Buffer.from(base64, 'base64').toString())
|
|
69
|
-
|
|
70
|
-
const now = Math.floor(Date.now() / 1000)
|
|
71
|
-
return payload.exp > now
|
|
72
|
-
}
|