Skip to content
Merged
25 changes: 24 additions & 1 deletion 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 @@ -521,12 +528,29 @@ const service = new ExampleServiceV1(options);
// 'service' can now be used to invoke operations.
```

To use the new service version with custom token lifetime:
```js
const authenticator = new VpcInstanceAuthenticator({
iamProfileCrn: 'crn:iam-profile-123',
serviceVersion: '2025-08-26',
tokenLifetime: 600,
});
```

### Configuration example
External configuration:
```
export EXAMPLE_SERVICE_AUTH_TYPE=vpc
export EXAMPLE_SERVICE_IAM_PROFILE_CRN=crn:iam-profile-123
```

To use the new service version:
```
export EXAMPLE_SERVICE_AUTH_TYPE=vpc
export EXAMPLE_SERVICE_IAM_PROFILE_CRN=crn:iam-profile-123
export EXAMPLE_SERVICE_VPC_IMS_VERSION=2025-08-26
```

Application code:
```js
const ExampleServiceV1 = require('<sdk-package-name>/example-service/v1');
Expand All @@ -540,7 +564,6 @@ const service = ExampleServiceV1.newInstance(options);
// 'service' can now be used to invoke operations.
```


## Cloud Pak for Data Authentication
The `CloudPakForDataAuthenticator` will accept a user-supplied username value, along with either a
password or apikey, and will
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;
/** The version of the Instance Metadata Service to be used obtaining tokens */
serviceVersion?: string;
/** The lifetime of the Instance Identity Token */
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
72 changes: 67 additions & 5 deletions auth/token-managers/vpc-instance-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,25 @@ import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager';

const DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254';
const METADATA_SERVICE_VERSION = '2022-03-01';
const METADATA_SERVICE_VERSION2 = '2025-08-26';
const IAM_EXPIRATION_WINDOW = 10;
const METADATA_TOKEN_LIFETIME = 300;
const DEFAULT_OPERATION_PATH_CREATE_ACCESS_TOKEN = '/instance_identity/v1/token';
const DEFAULT_OPERATION_PATH_CREATE_IAM_TOKEN = '/instance_identity/v1/iam_token';
const DEFAULT_OPERATION_PATH_CREATE_ACCESS_TOKEN2 = '/identity/v1/token';
const DEFAULT_OPERATION_PATH_CREATE_IAM_TOKEN2 = '/identity/v1/iam_tokens';
const metadataServiceSupportedVersions = [METADATA_SERVICE_VERSION, METADATA_SERVICE_VERSION2];

/** 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;
/** The version of the Instance Metadata Service to be used obtaining tokens */
serviceVersion?: string;
/** The lifetime of the Instance Identity Token */
tokenLifetime?: number;
}

// this interface is a representation of the response received from
Expand Down Expand Up @@ -57,6 +68,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager {

private iamProfileId: string;

private serviceVersion: string;

private tokenLifetime: number;

/**
* Create a new VpcInstanceTokenManager instance.
*
Expand All @@ -82,6 +97,22 @@ export class VpcInstanceTokenManager extends JwtTokenManager {

this.url = options.url || DEFAULT_IMS_ENDPOINT;

// Validate and set serviceVersion
const serviceVersion = options.serviceVersion || METADATA_SERVICE_VERSION;
if (!metadataServiceSupportedVersions.includes(serviceVersion)) {
throw new Error(
`Invalid serviceVersion. Must be one of: ${metadataServiceSupportedVersions.join(', ')}`
);
}
this.serviceVersion = serviceVersion;

// Validate and set tokenLifetime
const tokenLifetime = options.tokenLifetime || METADATA_TOKEN_LIFETIME;
if (typeof tokenLifetime !== 'number' || tokenLifetime < 0) {
throw new Error('tokenLifetime must be a non-negative number');
}
this.tokenLifetime = tokenLifetime;

if (options.iamProfileCrn) {
this.iamProfileCrn = options.iamProfileCrn;
}
Expand All @@ -108,6 +139,36 @@ export class VpcInstanceTokenManager extends JwtTokenManager {
this.iamProfileId = iamProfileId;
}

public setServiceVersion(serviceVersion: string): void {
if (!metadataServiceSupportedVersions.includes(serviceVersion)) {
throw new Error(
`Invalid serviceVersion. Must be one of: ${metadataServiceSupportedVersions.join(', ')}`
);
}
this.serviceVersion = serviceVersion;
}

public setTokenLifetime(tokenLifetime: number): void {
if (typeof tokenLifetime !== 'number' || tokenLifetime < 0) {
throw new Error('tokenLifetime must be a non-negative number');
}
this.tokenLifetime = tokenLifetime;
Comment thread
pyrooka marked this conversation as resolved.
}

protected getAccessTokenPath(): string {
if (this.serviceVersion === METADATA_SERVICE_VERSION2) {
return DEFAULT_OPERATION_PATH_CREATE_ACCESS_TOKEN2;
}
return DEFAULT_OPERATION_PATH_CREATE_ACCESS_TOKEN;
}

protected getIamTokenPath(): string {
if (this.serviceVersion === METADATA_SERVICE_VERSION2) {
return DEFAULT_OPERATION_PATH_CREATE_IAM_TOKEN2;
}
return DEFAULT_OPERATION_PATH_CREATE_IAM_TOKEN;
}

protected async requestToken(): Promise<any> {
const instanceIdentityToken: string = await this.getInstanceIdentityToken();

Expand All @@ -125,9 +186,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 @@ -136,6 +197,7 @@ export class VpcInstanceTokenManager extends JwtTokenManager {
'User-Agent': this.userAgent,
Accept: 'application/json',
Authorization: `Bearer ${instanceIdentityToken}`,
'Metadata-Flavor': 'ibm',
},
},
};
Expand All @@ -150,12 +212,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
109 changes: 109 additions & 0 deletions test/unit/vpc-instance-authenticator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
const { Authenticator, VpcInstanceAuthenticator } = require('../../dist/auth');
const { VpcInstanceTokenManager } = require('../../dist/auth');

// Constants for repeated values
const SERVICE_VERSION_2022 = '2022-03-01';
const SERVICE_VERSION_2025 = '2025-08-26';
const DEFAULT_TOKEN_LIFETIME = 300;
const CUSTOM_TOKEN_LIFETIME = 600;
const INVALID_SERVICE_VERSION_ERROR = `Invalid serviceVersion. Must be one of: ${SERVICE_VERSION_2022}, ${SERVICE_VERSION_2025}`;

// mock the `getToken` method in the token manager - dont make any rest calls
const fakeToken = 'iam-acess-token';
const mockedTokenManager = new VpcInstanceTokenManager();
Expand Down Expand Up @@ -76,6 +83,108 @@ 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: SERVICE_VERSION_2025,
tokenLifetime: CUSTOM_TOKEN_LIFETIME,
});

expect(authenticator.serviceVersion).toBe(SERVICE_VERSION_2025);
expect(authenticator.tokenManager.serviceVersion).toBe(SERVICE_VERSION_2025);
expect(authenticator.tokenLifetime).toBe(CUSTOM_TOKEN_LIFETIME);
expect(authenticator.tokenManager.tokenLifetime).toBe(CUSTOM_TOKEN_LIFETIME);
});

it('should use default serviceVersion and tokenLifetime when not provided', () => {
const authenticator = new VpcInstanceAuthenticator();

expect(authenticator.tokenManager.serviceVersion).toBe(SERVICE_VERSION_2022);
expect(authenticator.tokenManager.tokenLifetime).toBe(DEFAULT_TOKEN_LIFETIME);
});

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(SERVICE_VERSION_2022);

authenticator.setServiceVersion(SERVICE_VERSION_2025);
expect(authenticator.serviceVersion).toBe(SERVICE_VERSION_2025);

// also, verify that the underlying token manager has been updated
expect(authenticator.tokenManager.serviceVersion).toBe(SERVICE_VERSION_2025);
});

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(DEFAULT_TOKEN_LIFETIME);

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 tokenLifetime using the setter when already set in constructor', () => {
const authenticator = new VpcInstanceAuthenticator({
tokenLifetime: DEFAULT_TOKEN_LIFETIME,
});

expect(authenticator.tokenLifetime).toBe(DEFAULT_TOKEN_LIFETIME);
expect(authenticator.tokenManager.tokenLifetime).toBe(DEFAULT_TOKEN_LIFETIME);

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: SERVICE_VERSION_2025,
tokenLifetime: CUSTOM_TOKEN_LIFETIME,
};

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);
});

it('should accept serviceVersion from environment variables (via constructor)', () => {
// This simulates how environment variables are passed to the authenticator
// via getAuthenticatorFromEnvironment -> readExternalSources
const envConfig = {
iamProfileId: 'some-id',
serviceVersion: SERVICE_VERSION_2025, // This would come from SERVICE_NAME_SERVICE_VERSION env var
};

const authenticator = new VpcInstanceAuthenticator(envConfig);

expect(authenticator.serviceVersion).toBe(SERVICE_VERSION_2025);
expect(authenticator.tokenManager.serviceVersion).toBe(SERVICE_VERSION_2025);
expect(authenticator.iamProfileId).toBe('some-id');
});

it('should throw an error for invalid service version', () => {
expect(
() =>
new VpcInstanceAuthenticator({
serviceVersion: 'invalid-version',
})
).toThrow(INVALID_SERVICE_VERSION_ERROR);
});

// "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
Loading
Loading