diff --git a/Authentication.md b/Authentication.md index ea2df1dd..1240bf9a 100644 --- a/Authentication.md +++ b/Authentication.md @@ -464,6 +464,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). @@ -491,12 +498,27 @@ ExampleService service = new ExampleService(ExampleService.DEFAULT_SERVICE_NAME, // 'service' can now be used to invoke operations. ``` +To use the new service version with custom token lifetime: +```java +// Create the authenticator with new service version. +VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .serviceVersion("2025-08-26") + .tokenLifetime(600) + .build(); +``` + ### 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: ```java import .ExampleService.v1.ExampleService; diff --git a/src/main/java/com/ibm/cloud/sdk/core/security/Authenticator.java b/src/main/java/com/ibm/cloud/sdk/core/security/Authenticator.java index 68cfa2f2..9add2632 100644 --- a/src/main/java/com/ibm/cloud/sdk/core/security/Authenticator.java +++ b/src/main/java/com/ibm/cloud/sdk/core/security/Authenticator.java @@ -60,6 +60,7 @@ public interface Authenticator { String PROPNAME_IAM_PROFILE_ID = "IAM_PROFILE_ID"; String PROPNAME_IAM_PROFILE_NAME = "IAM_PROFILE_NAME"; String PROPNAME_IAM_ACCOUNT_ID = "IAM_ACCOUNT_ID"; + String PROPNAME_VPC_IMS_VERSION = "VPC_IMS_VERSION"; String PROPNAME_SCOPE_COLLECTION_TYPE = "SCOPE_COLLECTION_TYPE"; String PROPNAME_SCOPE_ID = "SCOPE_ID"; String PROPNAME_INCLUDE_BUILTIN_ACTIONS = "INCLUDE_BUILTIN_ACTIONS"; diff --git a/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java b/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java index b8687c1e..c840fd49 100644 --- a/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java +++ b/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java @@ -37,6 +37,7 @@ public class AuthenticatorBase { "iamAccountId must be specified if and only if iamProfileName is specified"; public static final String ERRORMSG_PROP_INVALID_BOOL = "The %s property must be a valid boolean but was '%s'. Valid values are 'true' and 'false'."; + public static final String ERRORMSG_INVALID_SERVICE_VERSION = "Invalid service version. Supported values are: %s"; /** * Returns a "Basic" Authorization header value for the specified username and password. diff --git a/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java b/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java index 59e27852..bfb8d216 100644 --- a/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java +++ b/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java @@ -13,6 +13,8 @@ package com.ibm.cloud.sdk.core.security; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,15 +44,21 @@ public class VpcInstanceAuthenticator private static final String defaultIMSEndpoint = "http://169.254.169.254"; private static final String operationPathCreateAccessToken = "/instance_identity/v1/token"; private static final String operationPathCreateIamToken = "/instance_identity/v1/iam_token"; + private static final String operationPathCreateAccessToken2 = "/identity/v1/token"; + private static final String operationPathCreateIamToken2 = "/identity/v1/iam_tokens"; private static final String metadataFlavor = "ibm"; private static final String metadataServiceVersion = "2022-03-01"; private static final int instanceIdentityTokenLifetime = 300; + private ArrayList defaultServiceSupportedVersions = new ArrayList<>(List.of("2022-03-01", "2025-08-26")); + + // Properties specific to a VpcInstanceAuthenticator. private String iamProfileCrn; private String iamProfileId; private String url; - + private String serviceVersion; + private int tokenLifetime; /** * This Builder class is used to construct IamAuthenticator instances. @@ -59,6 +67,8 @@ public static class Builder { private String iamProfileCrn; private String iamProfileId; private String url; + private String serviceVersion; + private int tokenLifetime; // Default ctor. public Builder() { @@ -69,6 +79,8 @@ private Builder(VpcInstanceAuthenticator obj) { this.iamProfileCrn = obj.iamProfileCrn; this.iamProfileId = obj.iamProfileId; this.url = obj.url; + this.serviceVersion = obj.serviceVersion; + this.tokenLifetime = obj.tokenLifetime; } /** @@ -121,6 +133,27 @@ public Builder url(String url) { this.url = url; return this; } + + /** + * Sets the serviceVersion Property. + * + * @param serviceVersion the base service version to use with the service. + * @return the Builder + */ + public Builder serviceVersion(String serviceVersion) { + this.serviceVersion = serviceVersion; + return this; + } + + /** + * Sets the tokenLifetime Property. + * @param tokenLifetime the base token lifetime to use. + * @return the Builder + */ + public Builder tokenLifetime(int tokenLifetime) { + this.tokenLifetime = tokenLifetime; + return this; + } } // The default ctor is hidden to force the use of the non-default ctors. @@ -139,6 +172,8 @@ protected VpcInstanceAuthenticator(Builder builder) { this.iamProfileCrn = builder.iamProfileCrn; this.iamProfileId = builder.iamProfileId; this.url = builder.url; + this.serviceVersion = StringUtils.isEmpty(builder.serviceVersion) ? metadataServiceVersion : builder.serviceVersion; + this.tokenLifetime = builder.tokenLifetime == 0 ? instanceIdentityTokenLifetime : builder.tokenLifetime; this.validate(); } @@ -161,7 +196,8 @@ public Builder newBuilder() { */ public static VpcInstanceAuthenticator fromConfiguration(Map config) { return new Builder().iamProfileCrn(config.get(PROPNAME_IAM_PROFILE_CRN)) - .iamProfileId(config.get(PROPNAME_IAM_PROFILE_ID)).url(config.get(PROPNAME_URL)).build(); + .iamProfileId(config.get(PROPNAME_IAM_PROFILE_ID)).url(config.get(PROPNAME_URL)) + .serviceVersion(config.get(PROPNAME_VPC_IMS_VERSION)).build(); } /** @@ -174,6 +210,11 @@ public void validate() { throw new IllegalArgumentException( String.format(ERRORMSG_ATMOST_ONE_PROP_ERROR, "iamProfileCrn", "iamProfileId")); } + + if (!this.defaultServiceSupportedVersions.contains(this.serviceVersion)) { + throw new IllegalArgumentException( + String.format(ERRORMSG_INVALID_SERVICE_VERSION, this.defaultServiceSupportedVersions)); + } } /** @@ -236,10 +277,71 @@ protected void setURL(String url) { this.url = url; } + /** + * @return the VPC ServiceVersion configured in this Authenticator. + */ + public String getServiceVersion() { + return this.serviceVersion; + } + + /** + * Sets the ServiceVersion in this Authenticator. + * + * @return the VPC ServiceVersion + */ + protected void setServiceVersion(String serviceVersion) { + if (StringUtils.isEmpty(serviceVersion)) { + serviceVersion = metadataServiceVersion; + } + this.serviceVersion = serviceVersion; + } + + /** + * @return the TokenLifetime configured on this Authenticator. + */ + public int getTokenLifetime() { + return this.tokenLifetime; + } + + /** + * Sets the TokenLifetime in this Authenticator. + * @param tokenLifetime + */ + protected void setTokenLifetime(int tokenLifetime) { + if (tokenLifetime == 0) { + tokenLifetime = instanceIdentityTokenLifetime; + } + this.tokenLifetime = tokenLifetime; + } + private String getImsEndpoint() { return (StringUtils.isEmpty(this.url) ? defaultIMSEndpoint : this.url); } + /** + * Gets the operation path for creating an access token based on the service version. + * + * @return the correct access token path + */ + public String getCreateAccessTokenPath() { + if (this.serviceVersion.equals("2025-08-26")) { + return operationPathCreateAccessToken2; + } + return operationPathCreateAccessToken; + } + + /** + * Gets the operation path for creating an IAM token based on the service version. + * + * @return the correct IAM token path + */ + public String getCreateIamTokenPath() { + if (this.serviceVersion.equals("2025-08-26")) { + return operationPathCreateIamToken2; + } + return operationPathCreateIamToken; + } + /** * Fetches an IAM access token using the authenticator's configuration. * @@ -271,15 +373,15 @@ public String retrieveInstanceIdentityToken() throws Throwable { try { // Create a PUT request to retrieve the instance identity token. RequestBuilder builder = RequestBuilder - .put(RequestBuilder.resolveRequestUrl(getImsEndpoint(), operationPathCreateAccessToken)); + .put(RequestBuilder.resolveRequestUrl(getImsEndpoint(), this.getCreateAccessTokenPath())); // Set the params and request body. - builder.query("version", metadataServiceVersion); + builder.query("version", this.getServiceVersion()); builder.header(HttpHeaders.ACCEPT, HttpMediaType.APPLICATION_JSON); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_JSON); builder.header("Metadata-Flavor", metadataFlavor); - String requestBody = String.format("{\"expires_in\": %d}", instanceIdentityTokenLifetime); + String requestBody = String.format("{\"expires_in\": %d}", this.getTokenLifetime()); builder.bodyContent(requestBody, HttpMediaType.APPLICATION_JSON); // Invoke the VPC IMDS "create_access_token" operation. @@ -306,10 +408,10 @@ public IamToken retrieveIamAccessToken(String instanceIdentityToken) { try { // Create a POST request to retrieve the IAM access token. RequestBuilder builder = - RequestBuilder.post(RequestBuilder.resolveRequestUrl(getImsEndpoint(), operationPathCreateIamToken)); + RequestBuilder.post(RequestBuilder.resolveRequestUrl(getImsEndpoint(), this.getCreateIamTokenPath())); // Set the params and request body. - builder.query("version", metadataServiceVersion); + builder.query("version", this.serviceVersion); builder.header(HttpHeaders.ACCEPT, HttpMediaType.APPLICATION_JSON); builder.header(HttpHeaders.CONTENT_TYPE, HttpMediaType.APPLICATION_JSON); builder.header(HttpHeaders.AUTHORIZATION, "Bearer " + instanceIdentityToken); @@ -347,3 +449,4 @@ public IamToken retrieveIamAccessToken(String instanceIdentityToken) { return iamToken; } } + diff --git a/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java b/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java index 55fa4794..0c26c20b 100644 --- a/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java +++ b/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java @@ -55,6 +55,11 @@ public class VpcInstanceAuthenticatorTest extends BaseServiceUnitTest { private static final String mockIamProfileCrn = "crn:iam-profile:123"; private static final String mockIamProfileId = "iam-id-123"; + private static final String operationPathCreateAccessToken = "/instance_identity/v1/token"; + private static final String operationPathCreateIamToken = "/instance_identity/v1/iam_token"; + private static final String operationPathCreateAccessToken2 = "/identity/v1/token"; + private static final String operationPathCreateIamToken2 = "/identity/v1/iam_tokens"; + private static final String mockErrorResponseJson1 = "{\"errors\": [{\"message\": \"Your create_access_token request was bad.\", \"code\": \"invalid_parameter_value\"}]}"; private static final String mockErrorResponseJson2 = @@ -623,4 +628,71 @@ public void testAuthenticateResponseError2() throws Throwable { fail("Expected RuntimeException, not " + t.getClass().getSimpleName()); } } + + @Test + public void testVpcAuthServiceVersionDefaults() { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .build(); + assertNotNull(authenticator); + + assertEquals(authenticator.getServiceVersion(), "2022-03-01"); + assertEquals(authenticator.getTokenLifetime(), 300); + + assertEquals(operationPathCreateAccessToken, authenticator.getCreateAccessTokenPath()); + assertEquals(operationPathCreateIamToken, authenticator.getCreateIamTokenPath()); + } + + @Test + public void testVpcAuthServiceVersionBuilder() { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .serviceVersion("2025-08-26") + .tokenLifetime(600) + .build(); + assertNotNull(authenticator); + + assertEquals(authenticator.getServiceVersion(), "2025-08-26"); + assertEquals(authenticator.getTokenLifetime(), 600); + + assertEquals(operationPathCreateAccessToken2, authenticator.getCreateAccessTokenPath()); + assertEquals(operationPathCreateIamToken2, authenticator.getCreateIamTokenPath()); + } + + @Test + public void testVpcAuthServiceVersionFromMap() { + Map properties = new HashMap<>(); + properties.put(Authenticator.PROPNAME_VPC_IMS_VERSION, "2025-08-26"); + + VpcInstanceAuthenticator authenticator = VpcInstanceAuthenticator.fromConfiguration(properties); + assertNotNull(authenticator); + + assertEquals(authenticator.getServiceVersion(), "2025-08-26"); + + assertEquals(operationPathCreateAccessToken2, authenticator.getCreateAccessTokenPath()); + assertEquals(operationPathCreateIamToken2, authenticator.getCreateIamTokenPath()); + } + + @Test + public void testVpcAuthServiceVersionOldVersion() { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .serviceVersion("2022-03-01") + .build(); + assertNotNull(authenticator); + + assertEquals(authenticator.getServiceVersion(), "2022-03-01"); + + assertEquals(operationPathCreateAccessToken, authenticator.getCreateAccessTokenPath()); + assertEquals(operationPathCreateIamToken, authenticator.getCreateIamTokenPath()); + } + + @Test + public void testVpcAuthServiceVersionCustomVersion() { + try { + new VpcInstanceAuthenticator.Builder() + .serviceVersion("2024-01-01") + .build(); + fail("Expected build() to throw an exception!"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "Invalid service version. Supported values are: [2022-03-01, 2025-08-26]"); + } + } }