Github user alopresto commented on a diff in the pull request: https://github.com/apache/nifi/pull/2085#discussion_r155127820 --- Diff: nifi-nar-bundles/nifi-oauth-bundle/nifi-oauth/src/main/java/org/apache/nifi/oauth/AbstractOAuthControllerService.java --- @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.oauth; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.controller.AbstractControllerService; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.reporting.InitializationException; + + +public abstract class AbstractOAuthControllerService + extends AbstractControllerService implements OAuth2ClientService { + + protected String accessToken = null; + protected String refreshToken = null; + protected String tokenType = null; + protected long expiresIn = -1; + protected long expiresTime = -1; + protected long lastResponseTimestamp = -1; + protected Map<String, String> extraHeaders = new HashMap<String, String>(); + protected String authUrl = null; + protected long expireTimeSafetyNetSeconds = -1; + protected String accessTokenRespName = null; + protected String expireTimeRespName = null; + protected String expireInRespName = null; + protected String tokenTypeRespName = null; + protected String scopeRespName = null; + + public static final PropertyDescriptor AUTH_SERVER_URL = new PropertyDescriptor + .Builder().name("OAuth2 Authorization Server URL") + .displayName("OAuth2 Authorization Server") + .description("OAuth2 Authorization Server that grants access to the protected resources on the behalf of the resource owner.") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor RESPONSE_ACCESS_TOKEN_FIELD_NAME = new PropertyDescriptor + .Builder().name("JSON response 'access_token' name") + .displayName("JSON response 'access_token' name") + .description("Name of the field in the JSON response that contains the access token. IETF OAuth2 spec default is 'access_token' if your API provider's" + + " response field is different this is where you can change that.") + .defaultValue("access_token") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor RESPONSE_EXPIRE_TIME_FIELD_NAME = new PropertyDescriptor + .Builder().name("JSON response 'expire_time' name") + .displayName("JSON response 'expire_time' name") + .description("Name of the field in the JSON response that contains the expire time. IETF OAuth2 spec default is 'expire_time' if your API provider's" + + " response field is different this is where you can change that.") + .defaultValue("expire_time") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor RESPONSE_EXPIRE_IN_FIELD_NAME = new PropertyDescriptor + .Builder().name("JSON response 'expire_in' name") + .displayName("JSON response 'expire_in' name") + .description("Name of the field in the JSON response that contains the expire in. IETF OAuth2 spec default is 'expire_in' if your API provider's" + + " response field is different this is where you can change that.") + .defaultValue("expire_in") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor RESPONSE_TOKEN_TYPE_FIELD_NAME = new PropertyDescriptor + .Builder().name("JSON response 'token_type' name") + .displayName("JSON response 'token_type' name") + .description("Name of the field in the JSON response that contains the token type. IETF OAuth2 spec default is 'token_type' if your API provider's" + + " response field is different this is where you can change that.") + .defaultValue("token_type") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor RESPONSE_SCOPE_FIELD_NAME = new PropertyDescriptor + .Builder().name("JSON response 'scope' name") + .displayName("JSON response 'scope' name") + .description("Name of the field in the JSON response that contains the scope. IETF OAuth2 spec default is 'scope' if your API provider's" + + " response field is different this is where you can change that.") + .defaultValue("scope") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + public static final PropertyDescriptor EXPIRE_TIME_SAFETY_NET = new PropertyDescriptor + .Builder().name("Expire time safety net in seconds") + .displayName("Expire time safety net in seconds") + .description("There can be a chance that the authentication server and the NiFi agent clocks could be out of sync. Expires In is an OAuth standard " + + "that tells when the access token received will be invalidated. Comparing the current system clock to that value and understanding if the " + + "access token is still valid can be achieved this way. However since the clocks can be out of sync with the authentication server " + + "this property ensures that the access token can be refreshed at a configurable interval before it actual expires to ensure no downtime " + + "waiting on new tokens from the authentication server.") + .defaultValue("10") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected void onEnabled(final ConfigurationContext context) + throws InitializationException { + Map<PropertyDescriptor, String> allProperties = context.getProperties(); + Iterator<PropertyDescriptor> itr = allProperties.keySet().iterator(); + while (itr.hasNext()) { + PropertyDescriptor pd = itr.next(); + if (pd.isDynamic()) { + extraHeaders.put(pd.getName(), allProperties.get(pd)); + } + } + + this.authUrl = context.getProperty(AUTH_SERVER_URL).getValue(); + this.expireTimeSafetyNetSeconds = new Long(context.getProperty(EXPIRE_TIME_SAFETY_NET).getValue()); + + // Name of the OAuth2 spec fields that should be extracted from the JSON authentication response. While in theory every provider should be + // using the same names in the response JSON that rarely happens. These values are used to ensure that if the provider does NOT follow the + // spec the user can still use this integration approach instead of having to write something custom. + this.accessTokenRespName = context.getProperty(RESPONSE_ACCESS_TOKEN_FIELD_NAME).getValue(); + this.expireTimeRespName = context.getProperty(RESPONSE_EXPIRE_TIME_FIELD_NAME).getValue(); + this.expireInRespName = context.getProperty(RESPONSE_EXPIRE_IN_FIELD_NAME).getValue(); + this.tokenTypeRespName = context.getProperty(RESPONSE_TOKEN_TYPE_FIELD_NAME).getValue(); + this.scopeRespName = context.getProperty(RESPONSE_SCOPE_FIELD_NAME).getValue(); + } + + public boolean isOAuthTokenExpired() { + if (expiresTime == -1 && expiresIn == -1) + return true; + + if (expiresTime > 0) { + // Use the actual time that the token will authenticate + if ((expiresTime - (expireTimeSafetyNetSeconds * 1000)) <= System.currentTimeMillis()) { --- End diff -- Let's say `System.currentTimeMillis()` returns a static value *1000* representing "now" and `expiresTime` is set by the remote system and returns a static value *3000* (2 seconds after *now*). With the default value of `expireTimeSafetyNetSeconds` as *10*, if the clock jitter is non-existent or "positive" (i.e. the local system's clock is "ahead"/returns a higher value than the remote system), this will evaluate to `3000 - (10 * 1000) <= 1000` which is `-7000 <= 1000` and eventually `true`. However, if the clock jitter is "behind", for example "now" on the remote system is *9001* while it's only *1000* locally (8.001 seconds difference, which should be within the "safety net" range), the expression will be `11001 - (10 * 1000) <= 1000` which is `1001 <= 1000` which is false. I believe this needs to use absolute value evaluations to handle both positive and negative offsets.
---