1+ /*
2+ * Copyright 2025 Google LLC
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
17+ import com .google .auth .oauth2 .ExternalAccountSupplierContext ;
18+ import com .google .auth .oauth2 .GoogleCredentials ;
19+ import com .google .auth .oauth2 .IdentityPoolCredentials ;
20+ import com .google .auth .oauth2 .IdentityPoolSubjectTokenSupplier ;
21+ import com .google .cloud .storage .Bucket ;
22+ import com .google .cloud .storage .Storage ;
23+ import com .google .cloud .storage .StorageOptions ;
24+ import com .google .gson .Gson ;
25+ import com .google .gson .JsonObject ;
26+ import java .io .BufferedReader ;
27+ import java .io .DataOutputStream ;
28+ import java .io .IOException ;
29+ import java .io .InputStreamReader ;
30+ import java .net .HttpURLConnection ;
31+ import java .net .URL ;
32+ import java .nio .charset .StandardCharsets ;
33+ import java .util .Base64 ;
34+
35+ /**
36+ * This sample demonstrates how to use a custom subject token supplier to authenticate with Google
37+ * Cloud, using Okta as the identity provider.
38+ */
39+ public class CustomCredentialSupplierOktaWorkload {
40+
41+ public static void main (String [] args ) throws IOException {
42+ // TODO(Developer): Replace these variables with your actual values.
43+ String gcpWorkloadAudience = System .getenv ("GCP_WORKLOAD_AUDIENCE" );
44+ String serviceAccountImpersonationUrl =
45+ System .getenv ("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL" );
46+ String gcsBucketName = System .getenv ("GCS_BUCKET_NAME" );
47+ String oktaDomain = System .getenv ("OKTA_DOMAIN" );
48+ String oktaClientId = System .getenv ("OKTA_CLIENT_ID" );
49+ String oktaClientSecret = System .getenv ("OKTA_CLIENT_SECRET" );
50+
51+ if (gcpWorkloadAudience == null
52+ || serviceAccountImpersonationUrl == null
53+ || gcsBucketName == null
54+ || oktaDomain == null
55+ || oktaClientId == null
56+ || oktaClientSecret == null ) {
57+ System .out .println (
58+ "Missing required environment variables. Please check your environment settings. "
59+ + "Required: GCP_WORKLOAD_AUDIENCE, GCP_SERVICE_ACCOUNT_IMPERSONATION_URL, "
60+ + "GCS_BUCKET_NAME, OKTA_DOMAIN, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET" );
61+ return ;
62+ }
63+
64+ customCredentialSupplierOktaWorkload (
65+ gcpWorkloadAudience ,
66+ serviceAccountImpersonationUrl ,
67+ gcsBucketName ,
68+ oktaDomain ,
69+ oktaClientId ,
70+ oktaClientSecret );
71+ }
72+
73+ public static void customCredentialSupplierOktaWorkload (
74+ String gcpWorkloadAudience ,
75+ String serviceAccountImpersonationUrl ,
76+ String gcsBucketName ,
77+ String oktaDomain ,
78+ String oktaClientId ,
79+ String oktaClientSecret )
80+ throws IOException {
81+ // 1. Instantiate our custom supplier with Okta credentials.
82+ OktaClientCredentialsSupplier oktaSupplier =
83+ new OktaClientCredentialsSupplier (oktaDomain , oktaClientId , oktaClientSecret );
84+
85+ // 2. Instantiate an IdentityPoolCredentials with the required configuration.
86+ GoogleCredentials credentials =
87+ IdentityPoolCredentials .newBuilder ()
88+ .setAudience (gcpWorkloadAudience )
89+ .setSubjectTokenType ("urn:ietf:params:oauth:token-type:jwt" )
90+ .setTokenUrl ("https://sts.googleapis.com/v1/token" )
91+ .setSubjectTokenSupplier (oktaSupplier )
92+ .setServiceAccountImpersonationUrl (serviceAccountImpersonationUrl )
93+ .build ();
94+
95+ // 3. Use the credentials to make an authenticated request.
96+ Storage storage = StorageOptions .newBuilder ().setCredentials (credentials ).build ().getService ();
97+
98+ System .out .println ("[Test] Getting metadata for bucket: " + gcsBucketName + "..." );
99+ Bucket bucket = storage .get (gcsBucketName );
100+ System .out .println (" --- SUCCESS! ---" );
101+ System .out .println ("Successfully authenticated and retrieved bucket data:" );
102+ System .out .println (bucket .toString ());
103+ }
104+
105+ /**
106+ * A custom SubjectTokenSupplier that authenticates with Okta using the Client Credentials grant
107+ * flow.
108+ */
109+ private static class OktaClientCredentialsSupplier implements IdentityPoolSubjectTokenSupplier {
110+
111+ private final String oktaTokenUrl ;
112+ private final String clientId ;
113+ private final String clientSecret ;
114+ private String accessToken ;
115+ private long expiryTime ;
116+
117+ public OktaClientCredentialsSupplier (String domain , String clientId , String clientSecret ) {
118+ this .oktaTokenUrl = domain + "/oauth2/default/v1/token" ;
119+ this .clientId = clientId ;
120+ this .clientSecret = clientSecret ;
121+ System .out .println ("OktaClientCredentialsSupplier initialized." );
122+ }
123+
124+ /**
125+ * Main method called by the auth library. It will fetch a new token if one is not already
126+ * cached.
127+ */
128+ @ Override
129+ public String getSubjectToken (ExternalAccountSupplierContext context ) throws IOException {
130+ // Check if the current token is still valid (with a 60-second buffer).
131+ boolean isTokenValid = this .accessToken != null && System .currentTimeMillis () < this .expiryTime - 60 * 1000 ;
132+
133+ if (isTokenValid ) {
134+ System .out .println ("[Supplier] Returning cached Okta Access token." );
135+ return this .accessToken ;
136+ }
137+
138+ System .out .println (
139+ "[Supplier] Token is missing or expired. Fetching new Okta Access token via Client "
140+ + "Credentials grant..." );
141+ fetchOktaAccessToken ();
142+ return this .accessToken ;
143+ }
144+
145+ /**
146+ * Performs the Client Credentials grant flow by making a POST request to Okta's token
147+ * endpoint.
148+ */
149+ private void fetchOktaAccessToken () throws IOException {
150+ URL url = new URL (this .oktaTokenUrl );
151+ HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
152+ conn .setRequestMethod ("POST" );
153+ conn .setRequestProperty ("Content-Type" , "application/x-www-form-urlencoded" );
154+
155+ String auth = this .clientId + ":" + this .clientSecret ;
156+ String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
157+ conn .setRequestProperty ("Authorization" , "Basic " + encodedAuth );
158+
159+ conn .setDoOutput (true );
160+ try (DataOutputStream out = new DataOutputStream (conn .getOutputStream ())) {
161+ String params = "grant_type=client_credentials&scope=gcp.test.read" ;
162+ out .writeBytes (params );
163+ out .flush ();
164+ }
165+
166+ int responseCode = conn .getResponseCode ();
167+ if (responseCode == HttpURLConnection .HTTP_OK ) {
168+ try (BufferedReader in = new BufferedReader (new InputStreamReader (conn .getInputStream ()))) {
169+ StringBuilder response = new StringBuilder ();
170+ String line ;
171+ while ((line = in .readLine ()) != null ) {
172+ response .append (line );
173+ }
174+
175+ Gson gson = new Gson ();
176+ JsonObject jsonObject = gson .fromJson (response .toString (), JsonObject .class );
177+
178+ if (jsonObject .has ("access_token" ) && jsonObject .has ("expires_in" )) {
179+ this .accessToken = jsonObject .get ("access_token" ).getAsString ();
180+ int expiresIn = jsonObject .get ("expires_in" ).getAsInt ();
181+ this .expiryTime = System .currentTimeMillis () + expiresIn * 1000 ;
182+ System .out .println (
183+ "[Supplier] Successfully received Access Token from Okta. Expires in "
184+ + expiresIn
185+ + " seconds." );
186+ } else {
187+ throw new IOException ("Access token or expires_in not found in Okta response." );
188+ }
189+ }
190+ } else {
191+ throw new IOException (
192+ "Failed to authenticate with Okta using Client Credentials grant. Response code: "
193+ + responseCode );
194+ }
195+ }
196+ }
197+ }
0 commit comments