1616 */
1717package com .dloop .nifi .gcp .oauth2 ;
1818
19+ import com .google .api .client .http .ByteArrayContent ;
20+ import com .google .api .client .http .GenericUrl ;
21+ import com .google .api .client .http .HttpRequest ;
22+ import com .google .api .client .http .javanet .NetHttpTransport ;
23+ import com .google .api .gax .core .FixedCredentialsProvider ;
1924import com .google .auth .oauth2 .GoogleCredentials ;
25+ import com .google .auth .oauth2 .ImpersonatedCredentials ;
26+ import com .google .cloud .iam .credentials .v1 .IamCredentialsClient ;
27+ import com .google .cloud .iam .credentials .v1 .IamCredentialsSettings ;
28+ import com .google .cloud .iam .credentials .v1 .SignJwtRequest ;
29+ import com .google .gson .Gson ;
30+ import com .google .gson .JsonObject ;
31+ import com .google .gson .JsonParser ;
2032import java .io .IOException ;
33+ import java .time .Duration ;
34+ import java .time .Instant ;
2135import java .util .ArrayList ;
36+ import java .util .Arrays ;
2237import java .util .Collections ;
38+ import java .util .Date ;
2339import java .util .List ;
40+ import java .util .Map ;
41+ import org .apache .http .client .utils .URLEncodedUtils ;
42+ import org .apache .http .message .BasicNameValuePair ;
2443import org .apache .nifi .annotation .documentation .CapabilityDescription ;
2544import org .apache .nifi .annotation .documentation .SeeAlso ;
2645import org .apache .nifi .annotation .documentation .Tags ;
3655import org .apache .nifi .reporting .InitializationException ;
3756
3857@ Tags ({ "gcp" , "oauth2" , "provider" , "authorization" , "access token" , "http" })
39- @ CapabilityDescription ("Provides OAuth 2.0 access tokens for Google APIs." )
58+ @ CapabilityDescription ("Provides OAuth 2.0 access tokens for Google REST APIs." )
4059@ SeeAlso ({ OAuth2AccessTokenProvider .class , GCPCredentialsService .class })
4160public class GCPOauth2AccessTokenProvider
4261 extends AbstractControllerService
4362 implements OAuth2AccessTokenProvider {
4463
45- public static final PropertyDescriptor PROJECT_ID =
64+ private static final String DEFAULT_SCOPE =
65+ "https://www.googleapis.com/auth/cloud-platform" ;
66+
67+ public static final PropertyDescriptor SCOPE =
4668 new PropertyDescriptor .Builder ()
47- .name ("project-id " )
48- .displayName ("Project ID " )
69+ .name ("scope " )
70+ .displayName ("Scope " )
4971 .description (
50- "Creates a credential with the provided quota project. "
72+ "Whitespace-delimited, case-sensitive list of scopes of the access request (as per the OAuth 2.0 specification). More information: https://developers.google.com/identity/protocols/oauth2/scopes "
5173 )
5274 .required (false )
5375 .addValidator (StandardValidators .NON_EMPTY_VALIDATOR )
76+ .defaultValue (DEFAULT_SCOPE )
5477 .build ();
5578
56- public static final PropertyDescriptor SCOPE =
79+ public static final PropertyDescriptor SERVICE_ACCOUNT =
5780 new PropertyDescriptor .Builder ()
58- .name ("scope " )
59- .displayName ("Scope " )
81+ .name ("impersonate-service-account " )
82+ .displayName ("Impersonate Service Account " )
6083 .description (
61- "Space-delimited, case-sensitive list of scopes of the access request (as per the OAuth 2.0 specification) "
84+ "Allow credentials issued to a user or service account to impersonate another service account. The source project must enable the \" IAMCredentials \" API. \n Also, the target service account must grant the originating principal the \" Service Account Token Creator \" IAM role. More information: https://cloud.google.com/iam/docs/service-account-impersonation "
6285 )
6386 .required (false )
6487 .addValidator (StandardValidators .NON_EMPTY_VALIDATOR )
@@ -67,29 +90,46 @@ public class GCPOauth2AccessTokenProvider
6790 public static final PropertyDescriptor DELEGATE =
6891 new PropertyDescriptor .Builder ()
6992 .name ("delegate" )
70- .displayName ("Delegate" )
93+ .displayName ("Domain-wide delegation" )
94+ .description (
95+ "If the credentials support domain-wide delegation, creates a copy of the identity so that it impersonates the specified user; otherwise, returns the same instance. To enable domain-wide delegation it is necessary to use Google Workspace in your organization. More information: https://developers.google.com/cloud-search/docs/guides/delegation"
96+ )
97+ .required (false )
98+ .addValidator (StandardValidators .NON_EMPTY_VALIDATOR )
99+ .build ();
100+
101+ public static final PropertyDescriptor PROJECT_ID =
102+ new PropertyDescriptor .Builder ()
103+ .name ("project-id" )
104+ .displayName ("Project ID" )
71105 .description (
72- "If the credentials support domain-wide delegation, creates a copy of the identity so that it impersonates the specified user; otherwise, returns the same instance. "
106+ "Sets a custom quota/billing project for the JWT sign request from the IAM Credentials API. Important: The calling user or service account must have the serviceusage.services.use IAM permission for a project to be able to designate it as your quota project. This setting does not have an affect on the quota project of the REST API requests that will use the access token of this service. To set a custom quota project for the REST API requests set the custom header 'x-goog- user-project' as a dynamic property on the InvokeHTTP processor. More information: https://cloud.google.com/docs/quotas/set-quota-project#rest_request "
73107 )
74108 .required (false )
75109 .addValidator (StandardValidators .NON_EMPTY_VALIDATOR )
110+ .dependsOn (SERVICE_ACCOUNT )
111+ .dependsOn (DELEGATE )
76112 .build ();
77113
78114 private static final List <PropertyDescriptor > properties ;
79115
80116 static {
81117 final List <PropertyDescriptor > props = new ArrayList <>();
82118 props .add (GoogleUtils .GCP_CREDENTIALS_PROVIDER_SERVICE );
83- props .add (PROJECT_ID );
84119 props .add (SCOPE );
120+ props .add (SERVICE_ACCOUNT );
85121 props .add (DELEGATE );
122+ props .add (PROJECT_ID );
86123 properties = Collections .unmodifiableList (props );
87124 }
88125
89126 private volatile GoogleCredentials googleCredentials ;
90- private volatile String projectId ;
91- private volatile String [] scopes ;
127+ private volatile IamCredentialsClient iamCredentialsClient ;
128+ private volatile AccessToken accessToken ;
129+ private volatile String serviceAccount ;
130+ private volatile String scope ;
92131 private volatile String delegate ;
132+ private volatile String projectId ;
93133
94134 @ Override
95135 protected List <PropertyDescriptor > getSupportedPropertyDescriptors () {
@@ -98,62 +138,172 @@ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
98138
99139 @ OnEnabled
100140 public void onEnabled (final ConfigurationContext context )
101- throws InitializationException {
141+ throws InitializationException , IOException {
142+ // Reset processor
143+ googleCredentials = null ;
144+ iamCredentialsClient = null ;
145+ accessToken = null ;
146+
102147 // Get GCP credentials service
103148 GCPCredentialsService gcpCredentialsService = context
104149 .getProperty (GoogleUtils .GCP_CREDENTIALS_PROVIDER_SERVICE )
105150 .asControllerService (GCPCredentialsService .class );
106151
107- // Get GCP credentials
108- googleCredentials = gcpCredentialsService .getGoogleCredentials ();
152+ // Initialize credentials
153+ googleCredentials = gcpCredentialsService
154+ .getGoogleCredentials ()
155+ .createScoped (DEFAULT_SCOPE );
109156
110- // Change quota project if specified
157+ scope = context .getProperty (SCOPE ).getValue ().replaceAll ("\\ s+" , " " );
158+ serviceAccount = context .getProperty (SERVICE_ACCOUNT ).getValue ();
159+ delegate = context .getProperty (DELEGATE ).getValue ();
111160 projectId = context .getProperty (PROJECT_ID ).getValue ();
112- if (projectId != null && !projectId .isBlank ()) {
113- googleCredentials = googleCredentials .createWithQuotaProject (
114- projectId
115- );
116- }
117161
118- // Apply required scope(s)
119- String scope = context .getProperty (SCOPE ).getValue ();
120- if (scope != null ) {
121- scopes = scope .split ("\\ s+" );
122- if (scopes .length > 0 ) {
123- googleCredentials = googleCredentials .createScoped (scopes );
162+ if (serviceAccount != null ) {
163+ // Impersonate service account with user delegation
164+ if (delegate != null ) {
165+ IamCredentialsSettings .Builder builder =
166+ IamCredentialsSettings .newBuilder ()
167+ .setCredentialsProvider (
168+ FixedCredentialsProvider .create (googleCredentials )
169+ );
170+
171+ if (projectId != null ) {
172+ builder .setQuotaProjectId (projectId );
173+ }
174+
175+ iamCredentialsClient = IamCredentialsClient .create (
176+ builder .build ()
177+ );
178+
179+ return ;
124180 }
181+
182+ // Impersonate service account without user delegation
183+ googleCredentials = ImpersonatedCredentials .newBuilder ()
184+ .setSourceCredentials (googleCredentials )
185+ .setTargetPrincipal (serviceAccount )
186+ .setScopes (Arrays .asList (scope .split (" " )))
187+ .build ();
188+
189+ return ;
125190 }
126191
127- // Impersonate a user account via account wide delegation if specified
128- delegate = context . getProperty ( DELEGATE ). getValue ();
129- if (delegate != null && ! delegate . isBlank () ) {
192+ googleCredentials = googleCredentials . createScoped ( scope . split ( " " ));
193+
194+ if (delegate != null ) {
130195 googleCredentials = googleCredentials .createDelegated (delegate );
131196 }
132197 }
133198
134199 @ Override
135200 public AccessToken getAccessDetails () {
136- // Refresh access token if expired
201+ if (accessToken != null && !accessToken .isExpired ()) {
202+ return accessToken ;
203+ }
204+
205+ com .google .auth .oauth2 .AccessToken gcpAccessToken ;
206+ if (serviceAccount != null && delegate != null ) {
207+ gcpAccessToken = getJwtCredentials ();
208+ } else {
209+ gcpAccessToken = getDefaultCredentials ();
210+ }
211+
212+ Long expiresIn = Duration .between (
213+ Instant .now (),
214+ gcpAccessToken .getExpirationTime ().toInstant ()
215+ ).getSeconds ();
216+
217+ accessToken = new AccessToken (
218+ gcpAccessToken .getTokenValue (),
219+ null ,
220+ "OAuth2" ,
221+ expiresIn ,
222+ scope
223+ );
224+
225+ return accessToken ;
226+ }
227+
228+ private com .google .auth .oauth2 .AccessToken getDefaultCredentials () {
137229 try {
138230 googleCredentials .refreshIfExpired ();
231+
232+ return googleCredentials .getAccessToken ();
139233 } catch (IOException e ) {
140- getLogger ().error (e .getMessage ());
234+ getLogger ().error (e .getMessage (), e );
235+
141236 return null ;
142237 }
238+ }
143239
144- String accessToken = googleCredentials .getAccessToken ().getTokenValue ();
145- String tokenType = googleCredentials .getAuthenticationType ();
146- Long expiresIn =
147- googleCredentials .getAccessToken ().getExpirationTime ().getTime () -
148- System .currentTimeMillis ();
240+ private com .google .auth .oauth2 .AccessToken getJwtCredentials () {
241+ try {
242+ long iat = Instant .now ().getEpochSecond ();
243+ long exp = iat + 3600 ;
244+ String payload = new Gson ()
245+ .toJson (
246+ Map .of (
247+ "aud" ,
248+ "https://oauth2.googleapis.com/token" ,
249+ "iat" ,
250+ iat ,
251+ "exp" ,
252+ exp ,
253+ "iss" ,
254+ serviceAccount ,
255+ "scope" ,
256+ scope ,
257+ "sub" ,
258+ delegate
259+ )
260+ );
149261
150- // Return access token
151- return new AccessToken (
152- accessToken ,
153- null ,
154- tokenType ,
155- expiresIn ,
156- String .join (" " , scopes )
157- );
262+ SignJwtRequest signJwtRequest = SignJwtRequest .newBuilder ()
263+ .setName ("projects/-/serviceAccounts/" + serviceAccount )
264+ .setPayload (payload )
265+ .build ();
266+
267+ String assertion = iamCredentialsClient
268+ .signJwt (signJwtRequest )
269+ .getSignedJwt ();
270+
271+ String body = URLEncodedUtils .format (
272+ List .of (
273+ new BasicNameValuePair ("assertion" , assertion ),
274+ new BasicNameValuePair (
275+ "grant_type" ,
276+ "urn:ietf:params:oauth:grant-type:jwt-bearer"
277+ )
278+ ),
279+ "utf-8"
280+ );
281+
282+ HttpRequest httpRequest = new NetHttpTransport ()
283+ .createRequestFactory ()
284+ .buildPostRequest (
285+ new GenericUrl ("https://oauth2.googleapis.com/token" ),
286+ ByteArrayContent .fromString (
287+ "application/x-www-form-urlencoded" ,
288+ body
289+ )
290+ );
291+
292+ JsonObject json = JsonParser .parseString (
293+ httpRequest .execute ().parseAsString ()
294+ ).getAsJsonObject ();
295+
296+ return new com .google .auth .oauth2 .AccessToken (
297+ json .get ("access_token" ).getAsString (),
298+ Date .from (
299+ Instant .now ()
300+ .plusSeconds (json .get ("expires_in" ).getAsLong ())
301+ )
302+ );
303+ } catch (IOException e ) {
304+ getLogger ().error (e .getMessage (), e );
305+
306+ return null ;
307+ }
158308 }
159309}
0 commit comments