Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -533,6 +540,8 @@ const ExampleServiceV1 = require('<sdk-package-name>/example-service/v1');

const options = {
serviceName: 'example_service',
serviceVersion: '2025-08-26',
tokenLifetime: 600,
};

const service = ExampleServiceV1.newInstance(options);
Expand Down
26 changes: 26 additions & 0 deletions auth/authenticators/vpc-instance-authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -43,6 +47,10 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator {

private iamProfileId: string;

private serviceVersion: string;

private tokenLifetime: number;

/**
* Create a new VpcInstanceAuthenticator instance.
*
Expand All @@ -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.
Expand Down Expand Up @@ -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').
*
Expand Down
43 changes: 38 additions & 5 deletions auth/token-managers/vpc-instance-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ 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 {
/** The CRN of the linked trusted IAM profile to be used as the identity of the compute resource */
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
Expand Down Expand Up @@ -57,6 +62,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager {

private iamProfileId: string;

private serviceVersion: string;

private tokenLifetime: number;

/**
* Create a new VpcInstanceTokenManager instance.
*
Expand All @@ -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;
Expand Down Expand Up @@ -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<any> {
const instanceIdentityToken: string = await this.getInstanceIdentityToken();

Expand All @@ -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',
Expand All @@ -150,12 +183,12 @@ export class VpcInstanceTokenManager extends JwtTokenManager {
private async getInstanceIdentityToken(): Promise<string> {
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: {
Expand Down
12 changes: 12 additions & 0 deletions etc/ibm-cloud-sdk-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,18 +510,30 @@ 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;
}

// @public
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<any>;
setIamProfileCrn(iamProfileCrn: string): void;
setIamProfileId(iamProfileId: string): void;
// (undocumented)
setServiceVersion(serviceVersion: string): void;
// (undocumented)
setTokenLifetime(tokenLifetime: number): void;
}

// (No @packageDocumentation comment for this package)
Expand Down
93 changes: 93 additions & 0 deletions test/unit/vpc-instance-authenticator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
75 changes: 75 additions & 0 deletions test/unit/vpc-instance-token-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
Loading