From 8f3abdb908fe14102fa347bf9061c8b572da8767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:01:29 +0200 Subject: [PATCH 1/2] feat(auth): add support for new service version in VPC Instant Auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- Authentication.md | 9 ++ .../vpc-instance-authenticator.ts | 26 ++++++ .../vpc-instance-token-manager.ts | 43 ++++++++- etc/ibm-cloud-sdk-core.api.md | 12 +++ test/unit/vpc-instance-authenticator.test.js | 93 +++++++++++++++++++ test/unit/vpc-instance-token-manager.test.js | 75 +++++++++++++++ 6 files changed, 253 insertions(+), 5 deletions(-) diff --git a/Authentication.md b/Authentication.md index e4a36b7ca..bc238ad93 100644 --- a/Authentication.md +++ b/Authentication.md @@ -492,6 +492,13 @@ The IAM access token is added to each outbound request in the `Authorization` he The default value of this property is `http://169.254.169.254`. However, if the VPC Instance Metadata Service is configured with the HTTP Secure Protocol setting (`https`), then you should configure this property to be `https://api.metadata.cloud.ibm.com`. +- serviceVersion: (optional) The VPC Instance Metadata Service version to use. +The default value is `2022-03-01`. When set to `2025-08-26`, the authenticator will use the new API paths +(`/identity/v1/token` and `/identity/v1/iam_tokens`) instead of the legacy paths. + +- tokenLifetime: (optional) The lifetime (in seconds) of the instance identity token. +The default value is `300` seconds. This property can only be configured programmatically (not via environment variables). + Usage Notes: 1. At most one of `iamProfileCrn` or `iamProfileId` may be specified. The specified value must map to a trusted IAM profile that has been linked to the compute resource (virtual server instance). @@ -533,6 +540,8 @@ const ExampleServiceV1 = require('/example-service/v1'); const options = { serviceName: 'example_service', + serviceVersion: '2025-08-26', + tokenLifetime: 600, }; const service = ExampleServiceV1.newInstance(options); diff --git a/auth/authenticators/vpc-instance-authenticator.ts b/auth/authenticators/vpc-instance-authenticator.ts index 28a07bd33..99ceb746e 100644 --- a/auth/authenticators/vpc-instance-authenticator.ts +++ b/auth/authenticators/vpc-instance-authenticator.ts @@ -24,6 +24,10 @@ export interface Options extends BaseOptions { iamProfileCrn?: string; /** The ID of the linked trusted IAM profile to be used when obtaining the IAM access token */ iamProfileId?: string; + + serviceVersion: string; + + tokenLifetime: number; } /** @@ -43,6 +47,10 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { private iamProfileId: string; + private serviceVersion: string; + + private tokenLifetime: number; + /** * Create a new VpcInstanceAuthenticator instance. * @@ -68,6 +76,12 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { if (options.iamProfileId) { this.iamProfileId = options.iamProfileId; } + if (options.serviceVersion) { + this.serviceVersion = options.serviceVersion; + } + if (options.tokenLifetime) { + this.tokenLifetime = options.tokenLifetime + } // the param names are shared between the authenticator and the token // manager so we can just pass along the options object. @@ -97,6 +111,18 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { this.tokenManager.setIamProfileId(iamProfileId); } + public setServiceVersion(serviceVersion: string): void { + this.serviceVersion = serviceVersion; + + this.tokenManager.setServiceVersion(serviceVersion); + } + + public setTokenLifetime(tokenLifetime: number): void { + this.tokenLifetime = tokenLifetime; + + this.tokenManager.setTokenLifetime(tokenLifetime); + } + /** * Returns the authenticator's type ('vpc'). * diff --git a/auth/token-managers/vpc-instance-token-manager.ts b/auth/token-managers/vpc-instance-token-manager.ts index f435eac80..bdaaaa81c 100644 --- a/auth/token-managers/vpc-instance-token-manager.ts +++ b/auth/token-managers/vpc-instance-token-manager.ts @@ -22,6 +22,7 @@ import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; const DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254'; const METADATA_SERVICE_VERSION = '2022-03-01'; const IAM_EXPIRATION_WINDOW = 10; +const METADATA_TOKEN_LIFETIME = 300; /** Configuration options for VPC token retrieval. */ interface Options extends JwtTokenManagerOptions { @@ -29,6 +30,10 @@ interface Options extends JwtTokenManagerOptions { iamProfileCrn?: string; /** The ID of the linked trusted IAM profile to be used when obtaining the IAM access token */ iamProfileId?: string; + + serviceVersion: string; + + tokenLifetime: number; } // this interface is a representation of the response received from @@ -57,6 +62,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager { private iamProfileId: string; + private serviceVersion: string; + + private tokenLifetime: number; + /** * Create a new VpcInstanceTokenManager instance. * @@ -81,6 +90,8 @@ export class VpcInstanceTokenManager extends JwtTokenManager { } this.url = options.url || DEFAULT_IMS_ENDPOINT; + this.serviceVersion = options.serviceVersion || METADATA_SERVICE_VERSION + this.tokenLifetime = options.tokenLifetime || METADATA_TOKEN_LIFETIME if (options.iamProfileCrn) { this.iamProfileCrn = options.iamProfileCrn; @@ -108,6 +119,28 @@ export class VpcInstanceTokenManager extends JwtTokenManager { this.iamProfileId = iamProfileId; } + public setServiceVersion(serviceVersion: string): void { + this.serviceVersion = serviceVersion; + } + + public setTokenLifetime(tokenLifetime: number): void { + this.tokenLifetime = tokenLifetime; + } + + protected getAccessTokenPath(): string { + if ((this.serviceVersion) === '2025-08-26') { + return '/identity/v1/token'; + } + return '/instance_identity/v1/token'; + } + + protected getIamTokenPath(): string { + if ((this.serviceVersion) === '2025-08-26') { + return '/identity/v1/iam_tokens'; + } + return '/instance_identity/v1/iam_token'; + } + protected async requestToken(): Promise { const instanceIdentityToken: string = await this.getInstanceIdentityToken(); @@ -125,9 +158,9 @@ export class VpcInstanceTokenManager extends JwtTokenManager { const parameters = { options: { - url: `${this.url}/instance_identity/v1/iam_token`, + url: `${this.url}${this.getIamTokenPath()}`, qs: { - version: METADATA_SERVICE_VERSION, + version: this.serviceVersion, }, body, method: 'POST', @@ -150,12 +183,12 @@ export class VpcInstanceTokenManager extends JwtTokenManager { private async getInstanceIdentityToken(): Promise { const parameters = { options: { - url: `${this.url}/instance_identity/v1/token`, + url: `${this.url}${this.getAccessTokenPath()}`, qs: { - version: METADATA_SERVICE_VERSION, + version: this.serviceVersion, }, body: { - expires_in: 300, + expires_in: this.tokenLifetime, }, method: 'PUT', headers: { diff --git a/etc/ibm-cloud-sdk-core.api.md b/etc/ibm-cloud-sdk-core.api.md index 62d1c9536..995bcf7ae 100644 --- a/etc/ibm-cloud-sdk-core.api.md +++ b/etc/ibm-cloud-sdk-core.api.md @@ -510,6 +510,10 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { setIamProfileCrn(iamProfileCrn: string): void; setIamProfileId(iamProfileId: string): void; // (undocumented) + setServiceVersion(serviceVersion: string): void; + // (undocumented) + setTokenLifetime(tokenLifetime: number): void; + // (undocumented) protected tokenManager: VpcInstanceTokenManager; } @@ -517,11 +521,19 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { export class VpcInstanceTokenManager extends JwtTokenManager { // Warning: (ae-forgotten-export) The symbol "Options_9" needs to be exported by the entry point index.d.ts constructor(options: Options_9); + // (undocumented) + protected getAccessTokenPath(): string; + // (undocumented) + protected getIamTokenPath(): string; protected isTokenExpired(): boolean; // (undocumented) protected requestToken(): Promise; setIamProfileCrn(iamProfileCrn: string): void; setIamProfileId(iamProfileId: string): void; + // (undocumented) + setServiceVersion(serviceVersion: string): void; + // (undocumented) + setTokenLifetime(tokenLifetime: number): void; } // (No @packageDocumentation comment for this package) diff --git a/test/unit/vpc-instance-authenticator.test.js b/test/unit/vpc-instance-authenticator.test.js index cfce4400f..74f57edb3 100644 --- a/test/unit/vpc-instance-authenticator.test.js +++ b/test/unit/vpc-instance-authenticator.test.js @@ -76,6 +76,99 @@ describe('VPC Instance Authenticator', () => { expect(authenticator.tokenManager.iamProfileId).toEqual(config.iamProfileId); }); + it('should store serviceVersion and tokenLifetime when provided in config', () => { + const authenticator = new VpcInstanceAuthenticator({ + serviceVersion: '2025-05-26', + tokenLifetime: 600, + }); + + expect(authenticator.serviceVersion).toBe('2025-05-26'); + expect(authenticator.tokenManager.serviceVersion).toBe('2025-05-26'); + expect(authenticator.tokenLifetime).toBe(600); + expect(authenticator.tokenManager.tokenLifetime).toBe(600); + }); + + it('should use default serviceVersion and tokenLifetime when not provided', () => { + const authenticator = new VpcInstanceAuthenticator(); + + expect(authenticator.tokenManager.serviceVersion).toBe('2022-03-01'); + expect(authenticator.tokenManager.tokenLifetime).toBe(300); + }); + + it('should set serviceVersion using the setter even when not declared in constructor', () => { + const authenticator = new VpcInstanceAuthenticator(); + + // Initially should be undefined on authenticator (but token manager has default) + expect(authenticator.serviceVersion).toBeUndefined(); + expect(authenticator.tokenManager.serviceVersion).toBe('2022-03-01'); + + authenticator.setServiceVersion('2025-05-26'); + expect(authenticator.serviceVersion).toBe('2025-05-26'); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.serviceVersion).toBe('2025-05-26'); + }); + + it('should set tokenLifetime using the setter even when not declared in constructor', () => { + const authenticator = new VpcInstanceAuthenticator(); + + // Initially should be undefined on authenticator (but token manager has default) + expect(authenticator.tokenLifetime).toBeUndefined(); + expect(authenticator.tokenManager.tokenLifetime).toBe(300); + + authenticator.setTokenLifetime(900); + expect(authenticator.tokenLifetime).toBe(900); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.tokenLifetime).toBe(900); + }); + + it('should re-set serviceVersion using the setter when already set in constructor', () => { + const authenticator = new VpcInstanceAuthenticator({ + serviceVersion: '2022-03-01', + }); + + expect(authenticator.serviceVersion).toBe('2022-03-01'); + expect(authenticator.tokenManager.serviceVersion).toBe('2022-03-01'); + + authenticator.setServiceVersion('2025-05-26'); + expect(authenticator.serviceVersion).toBe('2025-05-26'); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.serviceVersion).toBe('2025-05-26'); + }); + + it('should re-set tokenLifetime using the setter when already set in constructor', () => { + const authenticator = new VpcInstanceAuthenticator({ + tokenLifetime: 300, + }); + + expect(authenticator.tokenLifetime).toBe(300); + expect(authenticator.tokenManager.tokenLifetime).toBe(300); + + authenticator.setTokenLifetime(900); + expect(authenticator.tokenLifetime).toBe(900); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.tokenLifetime).toBe(900); + }); + + it('should pass all config options to token manager', () => { + const fullConfig = { + iamProfileId: 'some-id', + url: 'someurl.com', + serviceVersion: '2025-05-26', + tokenLifetime: 600, + }; + + const authenticator = new VpcInstanceAuthenticator(fullConfig); + + expect(authenticator.tokenManager.iamProfileId).toBe(fullConfig.iamProfileId); + expect(authenticator.tokenManager.url).toBe(fullConfig.url); + expect(authenticator.tokenManager.serviceVersion).toBe(fullConfig.serviceVersion); + expect(authenticator.tokenManager.tokenLifetime).toBe(fullConfig.tokenLifetime); + }); + // "end to end" style test, to make sure this authenticator integrates properly with parent classes it('should update the options and resolve with `null` when `authenticate` is called', async () => { const authenticator = new VpcInstanceAuthenticator({ iamProfileCrn: config.iamProfileCrn }); diff --git a/test/unit/vpc-instance-token-manager.test.js b/test/unit/vpc-instance-token-manager.test.js index 4b3cd64d0..9d5ad5ffc 100644 --- a/test/unit/vpc-instance-token-manager.test.js +++ b/test/unit/vpc-instance-token-manager.test.js @@ -231,7 +231,82 @@ describe('VPC Instance Token Manager', () => { /^ibm-node-sdk-core\/vpc-instance-authenticator.*$/ ); }); + + it('should use default service version and token lifetime', () => { + const instance = new VpcInstanceTokenManager(); + + // Test default service version + expect(instance.serviceVersion).toBe('2022-03-01'); + + // Test default token lifetime + expect(instance.tokenLifetime).toBe(300); + + // Test default paths for old service version + expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); + expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token'); + }); + + it('should use custom service version and token lifetime', () => { + const instance = new VpcInstanceTokenManager({ + serviceVersion: '2025-08-26', + tokenLifetime: 600, + }); + + // Test custom service version + expect(instance.serviceVersion).toBe('2025-08-26'); + + // Test custom token lifetime + expect(instance.tokenLifetime).toBe(600); + + // Test new paths for new service version + expect(instance.getAccessTokenPath()).toBe('/identity/v1/token'); + expect(instance.getIamTokenPath()).toBe('/identity/v1/iam_tokens'); + }); + + it('should set service version and token lifetime with setters', () => { + const instance = new VpcInstanceTokenManager(); + + instance.setServiceVersion('2025-08-26'); + instance.setTokenLifetime(600); + + // Test service version from setter + expect(instance.serviceVersion).toBe('2025-08-26'); + + // Test token lifetime from setter + expect(instance.tokenLifetime).toBe(600); + + // Test new paths for new service version + expect(instance.getAccessTokenPath()).toBe('/identity/v1/token'); + expect(instance.getIamTokenPath()).toBe('/identity/v1/iam_tokens'); + }); + + it('should use old paths for old service version', () => { + const instance = new VpcInstanceTokenManager({ + serviceVersion: '2022-03-01', + }); + + // Test old service version + expect(instance.serviceVersion).toBe('2022-03-01'); + + // Test old paths for old service version + expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); + expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token'); + }); + + it('should use old paths for non-2025-05-26 service version', () => { + const instance = new VpcInstanceTokenManager({ + serviceVersion: '2024-01-01', + }); + + // Test custom service version (not 2025-05-26) + expect(instance.serviceVersion).toBe('2024-01-01'); + + // Test old paths for non-2025-05-26 service version + expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); + expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token'); + }); }); + describe('getToken', () => { it('should refresh an expired access token', async () => { const instance = new VpcInstanceTokenManager({ iamProfileId: 'some-id' }); From 2c93bfd36315a5a9b824e841f49b46f65b3f3cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:04:43 +0200 Subject: [PATCH 2/2] chore: fix lint errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- .../vpc-instance-authenticator.ts | 4 +-- .../vpc-instance-token-manager.ts | 8 ++--- test/unit/vpc-instance-token-manager.test.js | 30 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/auth/authenticators/vpc-instance-authenticator.ts b/auth/authenticators/vpc-instance-authenticator.ts index 99ceb746e..b1e5e77a1 100644 --- a/auth/authenticators/vpc-instance-authenticator.ts +++ b/auth/authenticators/vpc-instance-authenticator.ts @@ -24,7 +24,7 @@ export interface Options extends BaseOptions { iamProfileCrn?: string; /** The ID of the linked trusted IAM profile to be used when obtaining the IAM access token */ iamProfileId?: string; - + serviceVersion: string; tokenLifetime: number; @@ -80,7 +80,7 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { this.serviceVersion = options.serviceVersion; } if (options.tokenLifetime) { - this.tokenLifetime = options.tokenLifetime + this.tokenLifetime = options.tokenLifetime; } // the param names are shared between the authenticator and the token diff --git a/auth/token-managers/vpc-instance-token-manager.ts b/auth/token-managers/vpc-instance-token-manager.ts index bdaaaa81c..621382a30 100644 --- a/auth/token-managers/vpc-instance-token-manager.ts +++ b/auth/token-managers/vpc-instance-token-manager.ts @@ -90,8 +90,8 @@ export class VpcInstanceTokenManager extends JwtTokenManager { } this.url = options.url || DEFAULT_IMS_ENDPOINT; - this.serviceVersion = options.serviceVersion || METADATA_SERVICE_VERSION - this.tokenLifetime = options.tokenLifetime || METADATA_TOKEN_LIFETIME + this.serviceVersion = options.serviceVersion || METADATA_SERVICE_VERSION; + this.tokenLifetime = options.tokenLifetime || METADATA_TOKEN_LIFETIME; if (options.iamProfileCrn) { this.iamProfileCrn = options.iamProfileCrn; @@ -128,14 +128,14 @@ export class VpcInstanceTokenManager extends JwtTokenManager { } protected getAccessTokenPath(): string { - if ((this.serviceVersion) === '2025-08-26') { + if (this.serviceVersion === '2025-08-26') { return '/identity/v1/token'; } return '/instance_identity/v1/token'; } protected getIamTokenPath(): string { - if ((this.serviceVersion) === '2025-08-26') { + if (this.serviceVersion === '2025-08-26') { return '/identity/v1/iam_tokens'; } return '/instance_identity/v1/iam_token'; diff --git a/test/unit/vpc-instance-token-manager.test.js b/test/unit/vpc-instance-token-manager.test.js index 9d5ad5ffc..fc52148d8 100644 --- a/test/unit/vpc-instance-token-manager.test.js +++ b/test/unit/vpc-instance-token-manager.test.js @@ -231,16 +231,16 @@ describe('VPC Instance Token Manager', () => { /^ibm-node-sdk-core\/vpc-instance-authenticator.*$/ ); }); - + it('should use default service version and token lifetime', () => { const instance = new VpcInstanceTokenManager(); - + // Test default service version expect(instance.serviceVersion).toBe('2022-03-01'); - + // Test default token lifetime expect(instance.tokenLifetime).toBe(300); - + // Test default paths for old service version expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token'); @@ -251,13 +251,13 @@ describe('VPC Instance Token Manager', () => { serviceVersion: '2025-08-26', tokenLifetime: 600, }); - + // Test custom service version expect(instance.serviceVersion).toBe('2025-08-26'); - + // Test custom token lifetime expect(instance.tokenLifetime).toBe(600); - + // Test new paths for new service version expect(instance.getAccessTokenPath()).toBe('/identity/v1/token'); expect(instance.getIamTokenPath()).toBe('/identity/v1/iam_tokens'); @@ -265,16 +265,16 @@ describe('VPC Instance Token Manager', () => { it('should set service version and token lifetime with setters', () => { const instance = new VpcInstanceTokenManager(); - + instance.setServiceVersion('2025-08-26'); instance.setTokenLifetime(600); - + // Test service version from setter expect(instance.serviceVersion).toBe('2025-08-26'); - + // Test token lifetime from setter expect(instance.tokenLifetime).toBe(600); - + // Test new paths for new service version expect(instance.getAccessTokenPath()).toBe('/identity/v1/token'); expect(instance.getIamTokenPath()).toBe('/identity/v1/iam_tokens'); @@ -284,10 +284,10 @@ describe('VPC Instance Token Manager', () => { const instance = new VpcInstanceTokenManager({ serviceVersion: '2022-03-01', }); - + // Test old service version expect(instance.serviceVersion).toBe('2022-03-01'); - + // Test old paths for old service version expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token'); @@ -297,10 +297,10 @@ describe('VPC Instance Token Manager', () => { const instance = new VpcInstanceTokenManager({ serviceVersion: '2024-01-01', }); - + // Test custom service version (not 2025-05-26) expect(instance.serviceVersion).toBe('2024-01-01'); - + // Test old paths for non-2025-05-26 service version expect(instance.getAccessTokenPath()).toBe('/instance_identity/v1/token'); expect(instance.getIamTokenPath()).toBe('/instance_identity/v1/iam_token');