2323import com .google .cloud .storage .Bucket ;
2424import com .google .cloud .storage .Storage ;
2525import com .google .cloud .storage .StorageOptions ;
26+ import java .time .Instant ;
2627import java .io .BufferedReader ;
2728import java .io .DataOutputStream ;
2829import java .io .IOException ;
@@ -44,7 +45,7 @@ public static void main(String[] args) {
4445 // 1. GCP_WORKLOAD_AUDIENCE:
4546 // The audience for the workload identity federation. This is the full resource name of the
4647 // Workload Identity Pool Provider, in the following format:
47- // //iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<pool-id>/providers/<provider-id>
48+ // ` //iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<pool-id>/providers/<provider-id>`
4849 String gcpWorkloadAudience = System .getenv ("GCP_WORKLOAD_AUDIENCE" );
4950
5051 // 2. GCP_SERVICE_ACCOUNT_IMPERSONATION_URL (optional):
@@ -58,7 +59,11 @@ public static void main(String[] args) {
5859 String gcsBucketName = System .getenv ("GCS_BUCKET_NAME" );
5960
6061 // 4. Okta Configuration:
61- // To set up the Okta application for this flow:
62+ // To set up the Okta application for this flow, refer:
63+ // https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/
64+ // https://developer.okta.com/docs/guides/customize-authz-server/main/
65+ //
66+ // Steps:
6267 // a. In your Okta developer console, create a new Application of type "Machine-to-Machine
6368 // (M2M)".
6469 // b. Under the "General" tab, ensure that "Client Credentials" is an allowed grant type.
@@ -138,11 +143,13 @@ public static void customCredentialSupplierOktaWorkload(
138143 */
139144 private static class OktaClientCredentialsSupplier implements IdentityPoolSubjectTokenSupplier {
140145
146+ private static final long TOKEN_REFRESH_BUFFER_SECONDS = 60 ;
147+
141148 private final String oktaTokenUrl ;
142149 private final String clientId ;
143150 private final String clientSecret ;
144151 private String accessToken ;
145- private long expiryTime ;
152+ private Instant expiryTime ;
146153
147154 public OktaClientCredentialsSupplier (String domain , String clientId , String clientSecret ) {
148155 this .oktaTokenUrl = domain + "/oauth2/default/v1/token" ;
@@ -159,7 +166,8 @@ public OktaClientCredentialsSupplier(String domain, String clientId, String clie
159166 public String getSubjectToken (ExternalAccountSupplierContext context ) throws IOException {
160167 // Check if the current token is still valid (with a 60-second buffer).
161168 boolean isTokenValid =
162- this .accessToken != null && System .currentTimeMillis () < this .expiryTime - 60 * 1000 ;
169+ this .accessToken != null
170+ && Instant .now ().isBefore (this .expiryTime .minusSeconds (TOKEN_REFRESH_BUFFER_SECONDS ));
163171
164172 if (isTokenValid ) {
165173 System .out .println ("[Supplier] Returning cached Okta Access token." );
@@ -178,59 +186,67 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
178186 */
179187 private void fetchOktaAccessToken () throws IOException {
180188 URL url = new URL (this .oktaTokenUrl );
181- HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
182- conn .setRequestMethod ("POST" );
183- conn .setRequestProperty ("Content-Type" , "application/x-www-form-urlencoded" );
184-
185- // The client_id and client_secret are sent in a Basic Auth header, as required by the
186- // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded.
187- String auth = this .clientId + ":" + this .clientSecret ;
188- String encodedAuth =
189- Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
190- conn .setRequestProperty ("Authorization" , "Basic " + encodedAuth );
191-
192- conn .setDoOutput (true );
193- try (DataOutputStream out = new DataOutputStream (conn .getOutputStream ())) {
194- // For the Client Credentials grant, scopes are optional and define the permissions
195- // the access token will have. Replace "gcp.test.read" with the scopes defined in your
196- // Okta authorization server. Multiple scopes can be requested by space-separating them
197- // (e.g., "scope1 scope2").
198- String params = "grant_type=client_credentials&scope=gcp.test.read" ;
199- out .writeBytes (params );
200- out .flush ();
201- }
202-
203- int responseCode = conn .getResponseCode ();
204- if (responseCode == HttpURLConnection .HTTP_OK ) {
205- try (BufferedReader in = new BufferedReader (new InputStreamReader (conn .getInputStream ()))) {
206- StringBuilder response = new StringBuilder ();
207- String line ;
208- while ((line = in .readLine ()) != null ) {
209- response .append (line );
210- }
189+ HttpURLConnection conn = null ;
190+ try {
191+ conn = (HttpURLConnection ) url .openConnection ();
192+ conn .setRequestMethod ("POST" );
193+ conn .setRequestProperty ("Content-Type" , "application/x-www-form-urlencoded" );
194+
195+ // The client_id and client_secret are sent in a Basic Auth header, as required by the
196+ // OAuth 2.0 Client Credentials grant specification. The credentials are Base64 encoded.
197+ String auth = this .clientId + ":" + this .clientSecret ;
198+ String encodedAuth =
199+ Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
200+ conn .setRequestProperty ("Authorization" , "Basic " + encodedAuth );
201+
202+ conn .setDoOutput (true );
203+ try (DataOutputStream out = new DataOutputStream (conn .getOutputStream ())) {
204+ // For the Client Credentials grant, scopes are optional and define the permissions
205+ // the access token will have. Replace "gcp.test.read" with the scopes defined in your
206+ // Okta authorization server. Multiple scopes can be requested by space-separating them.
207+ // In application/x-www-form-urlencoded, a space is represented by '+' or '%20'.
208+ // e.g., "scope1%20scope2" or "scope1+scope2".
209+ String params = "grant_type=client_credentials&scope=gcp.test.read%20gcp.bucket.read" ;
210+ out .writeBytes (params );
211+ out .flush ();
212+ }
211213
212- GenericJson jsonObject =
213- GsonFactory .getDefaultInstance ()
214- .createJsonParser (response .toString ())
215- .parse (GenericJson .class );
216-
217- if (jsonObject .containsKey ("access_token" ) && jsonObject .containsKey ("expires_in" )) {
218- this .accessToken = (String ) jsonObject .get ("access_token" );
219- Number expiresInNumber = (Number ) jsonObject .get ("expires_in" );
220- int expiresIn = expiresInNumber .intValue ();
221- this .expiryTime = System .currentTimeMillis () + expiresIn * 1000L ;
222- System .out .println (
223- "[Supplier] Successfully received Access Token from Okta. Expires in "
224- + expiresIn
225- + " seconds." );
226- } else {
227- throw new IOException ("Access token or expires_in not found in Okta response." );
214+ int responseCode = conn .getResponseCode ();
215+ if (responseCode == HttpURLConnection .HTTP_OK ) {
216+ try (BufferedReader in = new BufferedReader (new InputStreamReader (conn .getInputStream ()))) {
217+ StringBuilder response = new StringBuilder ();
218+ String line ;
219+ while ((line = in .readLine ()) != null ) {
220+ response .append (line );
221+ }
222+
223+ GenericJson jsonObject =
224+ GsonFactory .getDefaultInstance ()
225+ .createJsonParser (response .toString ())
226+ .parse (GenericJson .class );
227+
228+ if (jsonObject .containsKey ("access_token" ) && jsonObject .containsKey ("expires_in" )) {
229+ this .accessToken = (String ) jsonObject .get ("access_token" );
230+ Number expiresInNumber = (Number ) jsonObject .get ("expires_in" );
231+ int expiresIn = expiresInNumber .intValue ();
232+ this .expiryTime = Instant .now ().plusSeconds (expiresIn );
233+ System .out .println (
234+ "[Supplier] Successfully received Access Token from Okta. Expires in "
235+ + expiresIn
236+ + " seconds." );
237+ } else {
238+ throw new IOException ("Access token or expires_in not found in Okta response." );
239+ }
228240 }
241+ } else {
242+ throw new IOException (
243+ "Failed to authenticate with Okta using Client Credentials grant. Response code: "
244+ + responseCode );
245+ }
246+ } finally {
247+ if (conn != null ) {
248+ conn .disconnect ();
229249 }
230- } else {
231- throw new IOException (
232- "Failed to authenticate with Okta using Client Credentials grant. Response code: "
233- + responseCode );
234250 }
235251 }
236252 }
0 commit comments