@metaplay/metaplay-auth 1.4.2 → 1.6.0

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/package.json CHANGED
@@ -1,46 +1,47 @@
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.2",
4
+ "version": "1.6.0",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://metaplay.io",
8
8
  "bin": {
9
- "metaplay-auth": "dist/index.js"
9
+ "metaplay-auth": "dist/index.cjs"
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "tsx index.ts",
13
- "prepublish": "tsc"
13
+ "bake-version": "node -p \"'export const PACKAGE_VERSION = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@metaplay/eslint-config": "workspace:*",
20
- "@metaplay/typescript-config": "workspace:*",
21
- "@types/dockerode": "^3.3.28",
20
+ "@metaplay/prettier-config": "workspace:^",
21
+ "@prettier/plugin-pug": "workspace:^",
22
+ "@types/dockerode": "^3.3.31",
22
23
  "@types/express": "^4.17.21",
23
24
  "@types/js-yaml": "^4.0.9",
24
- "@types/jsonwebtoken": "^9.0.5",
25
+ "@types/jsonwebtoken": "^9.0.6",
25
26
  "@types/jwk-to-pem": "^2.0.3",
26
- "@types/node": "^20.12.5",
27
- "tsx": "^4.7.1",
28
- "typescript": "^5.1.6",
29
- "vitest": "^1.4.0"
30
- },
31
- "dependencies": {
32
- "@aws-sdk/client-ecr": "^3.549.0",
33
- "@kubernetes/client-node": "^1.0.0-rc4",
34
- "@ory/client": "^1.9.0",
27
+ "@types/node": "^20.16.1",
35
28
  "@types/semver": "^7.5.8",
36
- "commander": "^12.0.0",
29
+ "esbuild": "^0.23.1",
30
+ "tsx": "^4.19.0",
31
+ "typescript": "5.5.4",
32
+ "vitest": "^2.0.5",
33
+ "@aws-sdk/client-ecr": "^3.637.0",
34
+ "@kubernetes/client-node": "^1.0.0-rc6",
35
+ "@ory/client": "^1.14.5",
36
+ "commander": "^12.1.0",
37
37
  "dockerode": "^4.0.2",
38
- "h3": "^1.11.1",
38
+ "h3": "^1.12.0",
39
39
  "js-yaml": "^4.1.0",
40
40
  "jsonwebtoken": "^9.0.2",
41
- "jwk-to-pem": "^2.0.5",
42
- "open": "^10.1.0",
43
- "semver": "^7.6.0",
44
- "tslog": "^4.9.2"
41
+ "jwk-to-pem": "^2.0.6",
42
+ "open": "^8.4.2",
43
+ "semver": "^7.6.3",
44
+ "tslog": "^4.9.3",
45
+ "prettier": "3.3.3"
45
46
  }
46
47
  }
@@ -0,0 +1,3 @@
1
+ import MetaplayPrettierConfig from '@metaplay/prettier-config'
2
+
3
+ export default MetaplayPrettierConfig
package/src/auth.ts CHANGED
@@ -9,26 +9,36 @@ import jwt from 'jsonwebtoken'
9
9
  import jwkToPem from 'jwk-to-pem'
10
10
  import { Configuration, WellknownApi, OidcApi } from '@ory/client'
11
11
  import { setSecret, getSecret, removeSecret } from './secret_store.js'
12
-
12
+ import { portalBaseUrl } from './config.js'
13
13
  import { logger } from './logging.js'
14
14
 
15
+ export interface TokenSet {
16
+ id_token?: string
17
+ access_token: string
18
+ refresh_token?: string
19
+ }
20
+
15
21
  // oauth2 client details (maybe move these to be discovered from some online location to make changes easier to manage?)
16
22
  const clientId = 'c16ea663-ced3-46c6-8f85-38c9681fe1f0'
17
23
  const baseURL = 'https://auth.metaplay.dev'
18
24
  const authorizationEndpoint = `${baseURL}/oauth2/auth`
19
25
  const tokenEndpoint = `${baseURL}/oauth2/token`
20
- const wellknownApi = new WellknownApi(new Configuration({
21
- basePath: baseURL,
22
- }))
23
- const oidcApi = new OidcApi(new Configuration({
24
- basePath: baseURL,
25
- }))
26
+ const wellknownApi = new WellknownApi(
27
+ new Configuration({
28
+ basePath: baseURL,
29
+ })
30
+ )
31
+ const oidcApi = new OidcApi(
32
+ new Configuration({
33
+ basePath: baseURL,
34
+ })
35
+ )
26
36
 
27
37
  /**
28
38
  * A helper function which generates a code verifier and challenge for exchaning code from Ory server.
29
39
  * @returns
30
40
  */
31
- function generateCodeVerifierAndChallenge (): { verifier: string, challenge: string } {
41
+ function generateCodeVerifierAndChallenge(): { verifier: string; challenge: string } {
32
42
  const verifier: string = randomBytes(32).toString('hex')
33
43
  const challenge: string = createHash('sha256').update(verifier).digest('base64url')
34
44
  return { verifier, challenge }
@@ -39,7 +49,7 @@ function generateCodeVerifierAndChallenge (): { verifier: string, challenge: str
39
49
  * @param token The token to fetch the userinfo for.
40
50
  * @returns An object containing the user's info.
41
51
  */
42
- export async function getUserinfo (token: string): Promise<any> {
52
+ export async function getUserinfo(token: string): Promise<any> {
43
53
  logger.debug('Trying to find OIDC well-known endpoints...')
44
54
  const oidcRes = await oidcApi.discoverOidcConfiguration()
45
55
 
@@ -51,8 +61,8 @@ export async function getUserinfo (token: string): Promise<any> {
51
61
 
52
62
  const userinfoRes = await fetch(userinfoEndpoint, {
53
63
  headers: {
54
- Authorization: `Bearer ${token}`
55
- }
64
+ Authorization: `Bearer ${token}`,
65
+ },
56
66
  })
57
67
 
58
68
  if (userinfoRes.status < 200 || userinfoRes.status >= 300) {
@@ -66,14 +76,14 @@ export async function getUserinfo (token: string): Promise<any> {
66
76
  * A helper function which finds an local available port to listen on.
67
77
  * @returns A promise that resolves to an available port.
68
78
  */
69
- async function findAvailablePort (): Promise<number> {
79
+ async function findAvailablePort(): Promise<number> {
70
80
  return await new Promise((resolve, reject) => {
71
81
  // Ports need to be in sync with oauth2 client callbacks.
72
82
  const portsToCheck = [5000, 5001, 5002, 5003, 5004]
73
83
  let index = 0
74
84
 
75
85
  // Test ports by opening a server on them.
76
- function tryNextPort () {
86
+ function tryNextPort(): void {
77
87
  if (index >= portsToCheck.length) {
78
88
  reject(new Error('Could not find an available port.'))
79
89
  }
@@ -103,7 +113,7 @@ async function findAvailablePort (): Promise<number> {
103
113
  /**
104
114
  * Log in and save tokens to the local secret store.
105
115
  */
106
- export async function loginAndSaveTokens () {
116
+ export async function loginAndSaveTokens(): Promise<void> {
107
117
  // Find an available port to listen on.
108
118
  const availablePort = await findAvailablePort()
109
119
 
@@ -113,52 +123,57 @@ export async function loginAndSaveTokens () {
113
123
  const state = randomBytes(16).toString('hex')
114
124
 
115
125
  // Create a /callback endpoint that exchanges the code for tokens.
116
- app.use('/callback', defineEventHandler(async event => {
117
- // Read the query parameters.
118
- const {
119
- error,
120
- error_description: errorDescription,
121
- code
122
- } = getQuery(event)
123
-
124
- // Raise an error if the query parameters contain an error message.
125
- if (error) {
126
- console.error(`Error logging in. Received the following error:\n\n${String(error)}: ${String(errorDescription)}`)
127
- sendError(event, new Error(`Authentication failed: ${String(error)}: ${String(errorDescription)}`))
128
- server.close()
129
- process.exit(1)
130
- }
126
+ app.use(
127
+ '/callback',
128
+ defineEventHandler(async (event) => {
129
+ // Read the query parameters.
130
+ const { error, error_description: errorDescription, code } = getQuery(event)
131
+
132
+ // Raise an error if the query parameters contain an error message.
133
+ if (error) {
134
+ console.error(
135
+ `Error logging in. Received the following error:\n\n${String(error)}: ${String(errorDescription)}`
136
+ )
137
+ sendError(event, new Error(`Authentication failed: ${String(error)}: ${String(errorDescription)}`))
138
+ server.close()
139
+ process.exit(1)
140
+ }
131
141
 
132
- // Raise an error if the query parameters do not contain a code.
133
- if (typeof code !== 'string') {
134
- console.error('Error logging in. No code received.')
135
- sendError(event, new Error('Authentication failed: No code received.'))
136
- server.close()
137
- process.exit(1)
138
- }
142
+ // Raise an error if the query parameters do not contain a code.
143
+ if (typeof code !== 'string') {
144
+ console.error('Error logging in. No code received.')
145
+ sendError(event, new Error('Authentication failed: No code received.'))
146
+ server.close()
147
+ process.exit(1)
148
+ }
139
149
 
140
- // Exchange the code for tokens.
141
- try {
142
- logger.debug(`Received callback request with code ${code}. Preparing to exchange for tokens...`)
143
- const tokens = await getTokensWithAuthorizationCode(state, redirectUri, verifier, code)
150
+ // Exchange the code for tokens.
151
+ try {
152
+ logger.debug(`Received callback request with code ${code}. Preparing to exchange for tokens...`)
153
+ const tokens = await getTokensWithAuthorizationCode(state, redirectUri, verifier, code)
144
154
 
145
- // Only save access_token, id_token, and refresh_token
146
- await saveTokens({ access_token: tokens.access_token, id_token: tokens.id_token, refresh_token: tokens.refresh_token })
155
+ // Only save access_token, id_token, and refresh_token
156
+ await saveTokens({
157
+ access_token: tokens.access_token,
158
+ id_token: tokens.id_token,
159
+ refresh_token: tokens.refresh_token,
160
+ })
147
161
 
148
- console.log('You are now logged in and can call the other commands.')
162
+ console.log('You are now logged in and can call the other commands.')
149
163
 
150
- // TODO: Could return a pre-generated HTML page instead of text?
151
- return 'Authentication successful! You can close this window.'
152
- } catch (error) {
153
- if (error instanceof Error) {
154
- console.error(`Error: ${error.message}`)
164
+ // TODO: Could return a pre-generated HTML page instead of text?
165
+ return 'Authentication successful! You can close this window.'
166
+ } catch (error) {
167
+ if (error instanceof Error) {
168
+ console.error(`Error: ${error.message}`)
169
+ }
170
+ } finally {
171
+ server.close()
155
172
  }
156
- } finally {
157
- server.close()
158
- }
159
173
 
160
- process.exit(0)
161
- }))
174
+ process.exit(0)
175
+ })
176
+ )
162
177
 
163
178
  // Start the server.
164
179
  // We use use H3 and adapt it to Node.js's http server.
@@ -167,12 +182,14 @@ export async function loginAndSaveTokens () {
167
182
  logger.debug(`Listening on port ${availablePort} and waiting for callback...`)
168
183
 
169
184
  // Open the browser to log in.
170
- const authorizationUrl: string = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=${encodeURIComponent('openid offline_access')}&state=${encodeURIComponent(state)}`
171
- 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`)
185
+ const authorizationUrl = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256&scope=${encodeURIComponent('openid offline_access')}&state=${encodeURIComponent(state)}`
186
+ console.log(
187
+ `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`
188
+ )
172
189
  void open(authorizationUrl)
173
190
  }
174
191
 
175
- export async function machineLoginAndSaveTokens (clientId: string, clientSecret: string) {
192
+ export async function machineLoginAndSaveTokens(clientId: string, clientSecret: string): Promise<void> {
176
193
  // Get a fresh access token from Metaplay Auth.
177
194
  const params = new URLSearchParams()
178
195
  params.set('grant_type', 'client_credentials')
@@ -189,27 +206,29 @@ export async function machineLoginAndSaveTokens (clientId: string, clientSecret:
189
206
  })
190
207
 
191
208
  // Return type checked manually by Teemu on 2024-3-7.
192
- const tokens = await res.json() as { access_token: string, token_type: string, expires_in: number, scope: string }
209
+ const tokens = (await res.json()) as { access_token: string; token_type: string; expires_in: number; scope: string }
193
210
 
194
211
  logger.debug('Received machine authentication tokens, saving them for future use...')
195
212
 
196
213
  await saveTokens({ access_token: tokens.access_token })
197
214
 
198
- const userInfoResponse = await fetch('https://portal.metaplay.dev/api/external/userinfo', {
215
+ const userInfoResponse = await fetch(`${portalBaseUrl}/api/external/userinfo`, {
199
216
  headers: {
200
- Authorization: `Bearer ${tokens.access_token}`
217
+ Authorization: `Bearer ${tokens.access_token}`,
201
218
  },
202
219
  })
203
220
 
204
- const userInfo = await userInfoResponse.json() as { given_name: string, family_name: string }
221
+ const userInfo = (await userInfoResponse.json()) as { given_name: string; family_name: string }
205
222
 
206
- console.log(`You are now logged in with machine user ${userInfo.given_name} ${userInfo.family_name} (clientId=${clientId}) and can execute the other commands.`)
223
+ console.log(
224
+ `You are now logged in with machine user ${userInfo.given_name} ${userInfo.family_name} (clientId=${clientId}) and can execute the other commands.`
225
+ )
207
226
  }
208
227
 
209
228
  /**
210
229
  * Refresh access and ID token with a refresh token.
211
230
  */
212
- export async function extendCurrentSession (): Promise<void> {
231
+ export async function extendCurrentSession(): Promise<void> {
213
232
  try {
214
233
  const tokens = await loadTokens()
215
234
 
@@ -222,13 +241,19 @@ export async function extendCurrentSession (): Promise<void> {
222
241
 
223
242
  // Check that the refresh_token exists (machine users don't have it)
224
243
  if (!tokens.refresh_token) {
225
- throw new Error('Cannot refresh an access_token without a refresh_token. With machine users, should just login again instead.')
244
+ throw new Error(
245
+ 'Cannot refresh an access_token without a refresh_token. With machine users, should just login again instead.'
246
+ )
226
247
  }
227
248
 
228
249
  logger.debug('Access token is no longer valid, trying to extend the current session with a refresh token.')
229
250
  const refreshedTokens = await extendCurrentSessionWithRefreshToken(tokens.refresh_token)
230
251
 
231
- await saveTokens({ access_token: refreshedTokens.access_token, id_token: refreshedTokens.id_token, refresh_token: refreshedTokens.refresh_token })
252
+ await saveTokens({
253
+ access_token: refreshedTokens.access_token,
254
+ id_token: refreshedTokens.id_token,
255
+ refresh_token: refreshedTokens.refresh_token,
256
+ })
232
257
  } catch (error) {
233
258
  if (error instanceof Error) {
234
259
  console.error(error.message)
@@ -242,13 +267,15 @@ export async function extendCurrentSession (): Promise<void> {
242
267
  * @param refreshToken: The refresh token to use to get a new set of tokens.
243
268
  * @returns A promise that resolves to a new set of tokens.
244
269
  */
245
- async function extendCurrentSessionWithRefreshToken (refreshToken: string): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
270
+ async function extendCurrentSessionWithRefreshToken(
271
+ refreshToken: string
272
+ ): Promise<{ id_token: string; access_token: string; refresh_token: string }> {
246
273
  // TODO: similar to the todo task in getTokensWithAuthorizationCode, http request can be handled by ory/client.
247
274
  const params = new URLSearchParams({
248
275
  grant_type: 'refresh_token',
249
276
  refresh_token: refreshToken,
250
277
  scope: 'openid offline_access',
251
- client_id: clientId
278
+ client_id: clientId,
252
279
  })
253
280
 
254
281
  logger.debug('Refreshing tokens...')
@@ -268,14 +295,16 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
268
295
  logger.error(`Failed to refresh tokens via endpoint ${tokenEndpoint}`)
269
296
  logger.error('Fetch error details:', error)
270
297
  if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
271
- throw new Error(`Failed to refresh tokens: SSL certificate validation failed for ${tokenEndpoint}. Is someone trying to tamper with your internet connection?`)
298
+ throw new Error(
299
+ `Failed to refresh tokens: SSL certificate validation failed for ${tokenEndpoint}. Is someone trying to tamper with your internet connection?`
300
+ )
272
301
  }
273
302
  throw new Error(`Failed to refresh tokens via ${tokenEndpoint}: ${error}`)
274
303
  }
275
304
 
276
305
  // Check if the response is OK
277
306
  if (!response.ok) {
278
- const responseJSON = await response.json()
307
+ const responseJSON = (await response.json()) as { error: string; error_description: string }
279
308
 
280
309
  logger.error('Failed to refresh tokens.')
281
310
  logger.error(`Error Type: ${responseJSON.error}`)
@@ -288,7 +317,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
288
317
  throw new Error('Failed extending current session, exiting. Please log in again.')
289
318
  }
290
319
 
291
- return await response.json()
320
+ return (await response.json()) as { id_token: string; access_token: string; refresh_token: string }
292
321
  }
293
322
 
294
323
  /**
@@ -299,22 +328,29 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
299
328
  * @param code
300
329
  * @returns
301
330
  */
302
- async function getTokensWithAuthorizationCode (state: string, redirectUri: string, verifier: string, code: string): Promise<{ id_token: string, access_token: string, refresh_token: string }> {
331
+ // eslint-disable-next-line @typescript-eslint/max-params
332
+ async function getTokensWithAuthorizationCode(
333
+ state: string,
334
+ redirectUri: string,
335
+ verifier: string,
336
+ code: string
337
+ ): Promise<{ id_token: string; access_token: string; refresh_token: string }> {
303
338
  // TODO: the authorication code exchange flow might be better to be handled by ory/client, could check if there's any useful toosl there.
304
339
  try {
305
340
  const response = await fetch(tokenEndpoint, {
306
341
  method: 'POST',
307
342
  headers: {
308
- 'Content-Type': 'application/x-www-form-urlencoded'
343
+ 'Content-Type': 'application/x-www-form-urlencoded',
309
344
  },
310
- body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`
345
+ body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`,
311
346
  })
312
347
 
313
- return await response.json()
348
+ return (await response.json()) as { id_token: string; access_token: string; refresh_token: string }
314
349
  } catch (error) {
315
350
  if (error instanceof Error) {
316
351
  logger.error(`Error exchanging code for tokens: ${error.message}`)
317
352
  }
353
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
318
354
  throw error
319
355
  }
320
356
  }
@@ -322,9 +358,9 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
322
358
  /**
323
359
  * Load tokens from the local secret store.
324
360
  */
325
- export async function loadTokens (): Promise<{ id_token?: string, access_token: string, refresh_token?: string }> {
361
+ export async function loadTokens(): Promise<TokenSet> {
326
362
  try {
327
- const tokens = await getSecret('tokens') as { id_token?: string, access_token: string, refresh_token?: string }
363
+ const tokens = (await getSecret('tokens')) as TokenSet
328
364
 
329
365
  if (!tokens) {
330
366
  throw new Error('Unable to load tokens. You need to login first.')
@@ -335,6 +371,7 @@ export async function loadTokens (): Promise<{ id_token?: string, access_token:
335
371
  if (error instanceof Error) {
336
372
  throw new Error(`Error loading tokens: ${error.message}`)
337
373
  }
374
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
338
375
  throw error
339
376
  }
340
377
  }
@@ -343,13 +380,15 @@ export async function loadTokens (): Promise<{ id_token?: string, access_token:
343
380
  * Save tokens to the local secret store.
344
381
  * @param tokens The tokens to save.
345
382
  */
346
- export async function saveTokens (tokens: Record<string, string>): Promise<void> {
383
+ export async function saveTokens(tokens: Record<string, string>): Promise<void> {
347
384
  try {
348
385
  logger.debug('Received new tokens, verifying...')
349
386
 
350
387
  // All tokens must have an access_token (machine users only have it)
351
388
  if (!tokens.access_token) {
352
- throw new Error('Metaplay token has no access_token. Please log in again and make sure all checkboxes of permissions are selected before proceeding.')
389
+ throw new Error(
390
+ 'Metaplay token has no access_token. Please log in again and make sure all checkboxes of permissions are selected before proceeding.'
391
+ )
353
392
  }
354
393
 
355
394
  logger.debug('Token verification completed, storing tokens...')
@@ -363,6 +402,7 @@ export async function saveTokens (tokens: Record<string, string>): Promise<void>
363
402
  if (error instanceof Error) {
364
403
  throw new Error(`Failed to save tokens: ${error.message}`)
365
404
  }
405
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
366
406
  throw error
367
407
  }
368
408
  }
@@ -370,7 +410,7 @@ export async function saveTokens (tokens: Record<string, string>): Promise<void>
370
410
  /**
371
411
  * Remove tokens from the local secret store.
372
412
  */
373
- export async function removeTokens (): Promise<void> {
413
+ export async function removeTokens(): Promise<void> {
374
414
  try {
375
415
  await removeSecret('tokens')
376
416
  logger.debug('Removed tokens.')
@@ -378,6 +418,7 @@ export async function removeTokens (): Promise<void> {
378
418
  if (error instanceof Error) {
379
419
  throw new Error(`Error removing tokens: ${error.message}`)
380
420
  }
421
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
381
422
  throw error
382
423
  }
383
424
  }
@@ -387,7 +428,7 @@ export async function removeTokens (): Promise<void> {
387
428
  * @param token The token being validated
388
429
  * @returns return if token is valid, throw errors otherwise
389
430
  */
390
- async function validateToken (token: string): Promise<boolean> {
431
+ async function validateToken(token: string): Promise<boolean> {
391
432
  try {
392
433
  // Decode the token
393
434
  const completeTokenData = jwt.decode(token, { complete: true })
@@ -422,7 +463,7 @@ async function validateToken (token: string): Promise<boolean> {
422
463
  * A helper function which shows token info after being fully decoded.
423
464
  * @param token The token to show info for.
424
465
  */
425
- async function showTokenInfo (token: string): Promise<void> {
466
+ async function showTokenInfo(token: string): Promise<void> {
426
467
  logger.debug('Showing access token info...')
427
468
  // Decode the token
428
469
  const completeTokenData = jwt.decode(token, { complete: true })