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