Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright © 2024 Cask Data, Inc.
* Licensed 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 io.cdap.cdap.datapipeline.oauth;

/**
* The type of OAuth 2.0 authorization flow to use.
*/
public enum AuthType {
/**
* Standard OAuth 2.0 Authorization Code flow.
*/
STANDARD,

/**
* OAuth 2.0 Authorization Code flow with Proof Key for Code Exchange (PKCE).
* Provides enhanced security by dynamically generating a code challenge and verifier.
*/
PKCE
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,23 @@ public class OAuthProvider {
@Nullable
private final String userAgent;

@Nullable
private final AuthType authType;
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The authType field is annotated with @Nullable, but the constructor guarantees it will never be null by defaulting to AuthType.STANDARD if a null value is passed. To avoid confusion and accurately reflect the class invariants, the @Nullable annotation should be removed from the field.

Suggested change
@Nullable
private final AuthType authType;
private final AuthType authType;


public OAuthProvider(String name,
String loginURL,
String tokenRefreshURL,
@Nullable OAuthClientCredentials clientCreds,
@Nullable CredentialEncodingStrategy strategy,
@Nullable String userAgent) {
@Nullable String userAgent,
@Nullable AuthType authType) {
this.name = name;
this.loginURL = loginURL;
this.tokenRefreshURL = tokenRefreshURL;
this.clientCreds = clientCreds;
this.strategy = strategy;
this.userAgent = userAgent;
this.authType = authType != null ? authType : AuthType.STANDARD;
}

public String getName() {
Expand Down Expand Up @@ -74,6 +79,10 @@ public String getUserAgent() {
return userAgent;
}

public AuthType getAuthType() {
return authType;
}

public enum CredentialEncodingStrategy {
// (default) Sends client ID & secret as part of the POST request body
FORM_BODY,
Expand All @@ -95,6 +104,7 @@ public static class Builder {
private OAuthClientCredentials clientCreds;
private CredentialEncodingStrategy strategy;
private String userAgent;
private AuthType authType;

public Builder() {}

Expand Down Expand Up @@ -128,6 +138,11 @@ public Builder withUserAgent(@Nullable String userAgent) {
return this;
}

public Builder withAuthType(@Nullable AuthType authType) {
this.authType = authType;
return this;
}

public OAuthProvider build() {
Preconditions.checkNotNull(name, "OAuth provider name missing");
Preconditions.checkNotNull(loginURL, "Login URL missing");
Expand All @@ -136,7 +151,7 @@ public OAuthProvider build() {
if (strategy == null) {
this.strategy = CredentialEncodingStrategy.FORM_BODY;
}
return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent);
return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent, authType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ public class OAuthStore {
private static final String TOKEN_REFRESH_URL_COL = "tokenrefreshurl";
private static final String CREDENTIAL_ENCODING_STRATEGY_COL = "credentialencodingstrategy";
private static final String USER_AGENT_COL = "useragent";
private static final String AUTH_TYPE_COL = "authtype";
private static final String CLIENT_CREDS_KEY_PREFIX = "oauthclientcreds";
private static final String ACCESS_TOKEN_KEY_PREFIX = "oauthaccesstoken";
private static final String REFRESH_TOKEN_KEY_PREFIX = "oauthrefreshtoken";
private static final String PKCE_KEY_PREFIX = "oauth-pkce";
private static final Gson GSON = new Gson();
private final TransactionRunner transactionRunner;
private final SecureStore secureStore;
private final SecureStoreManager secureStoreManager;
private final long codeVerifierTTL;

public static final StructuredTableId TABLE_ID = new StructuredTableId("oauth");
public static final StructuredTableSpecification TABLE_SPEC = new StructuredTableSpecification.Builder()
Expand All @@ -61,17 +64,20 @@ public class OAuthStore {
Fields.stringType(LOGIN_URL_COL),
Fields.stringType(TOKEN_REFRESH_URL_COL),
Fields.stringType(CREDENTIAL_ENCODING_STRATEGY_COL),
Fields.stringType(USER_AGENT_COL))
Fields.stringType(USER_AGENT_COL),
Fields.stringType(AUTH_TYPE_COL))
.withPrimaryKeys(OAUTH_PROVIDER_COL)
.build();

public OAuthStore(
TransactionRunner transactionRunner,
SecureStore secureStore,
SecureStoreManager secureStoreManager) {
SecureStoreManager secureStoreManager,
long codeVerifierTTL) {
this.transactionRunner = transactionRunner;
this.secureStore = secureStore;
this.secureStoreManager = secureStoreManager;
this.codeVerifierTTL = codeVerifierTTL;
}

/**
Expand Down Expand Up @@ -299,6 +305,10 @@ private static String getAccessTokenKey(String oauthProvider, String credentialI
return String.format("%s-%s-%s", ACCESS_TOKEN_KEY_PREFIX, oauthProvider.toLowerCase(), credentialId.toLowerCase());
}

private static String getPKCEKey(String oauthProvider, String state) {
return String.format("%s-%s-%s", PKCE_KEY_PREFIX, oauthProvider.toLowerCase(), state.toLowerCase());
}

private static List<Field<?>> getKey(String name) {
List<Field<?>> keyFields = new ArrayList<>(1);
keyFields.add(Fields.stringField(OAUTH_PROVIDER_COL, name));
Expand All @@ -311,6 +321,7 @@ private static OAuthProvider fromRow(StructuredRow row, OAuthClientCredentials c
String tokenRefreshURL = row.getString(TOKEN_REFRESH_URL_COL);
String credentialEncodingStrategy = row.getString(CREDENTIAL_ENCODING_STRATEGY_COL);
String userAgent = row.getString(USER_AGENT_COL);
String authTypeStr = row.getString(AUTH_TYPE_COL);

return OAuthProvider.newBuilder()
.withName(name)
Expand All @@ -321,6 +332,7 @@ private static OAuthProvider fromRow(StructuredRow row, OAuthClientCredentials c
Optional.ofNullable(credentialEncodingStrategy)
.map(OAuthProvider.CredentialEncodingStrategy::valueOf).orElse(null))
.withUserAgent(userAgent)
.withAuthType(authTypeStr != null ? AuthType.valueOf(authTypeStr) : AuthType.STANDARD)
.build();
}

Expand All @@ -333,6 +345,36 @@ private static List<Field<?>> getRow(OAuthProvider oauthProvider) {
CREDENTIAL_ENCODING_STRATEGY_COL,
oauthProvider.getCredentialEncodingStrategy().toString()));
fields.add(Fields.stringField(USER_AGENT_COL, oauthProvider.getUserAgent()));
fields.add(Fields.stringField(AUTH_TYPE_COL, oauthProvider.getAuthType().toString()));
return fields;
}

public void writePKCECodeVerifier(String provider, String state, String codeVerifier) throws Exception {
String key = getPKCEKey(provider, state);
secureStoreManager.put(NamespaceId.SYSTEM.getNamespace(), key, codeVerifier,
"PKCE Code Verifier", Collections.emptyMap(), codeVerifierTTL);
}

public String getPKCECodeVerifier(String provider, String state) throws OAuthStoreException {
String key = getPKCEKey(provider, state);
try {
return new String(secureStore.getData(NamespaceId.SYSTEM.getNamespace(), key), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new OAuthStoreException("Failed to read PKCE code verifier from secure storage", e);
} catch (Exception e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch specific exception?

@sahusanket sahusanket Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done
In interface > Exception : if the specified namespace or name does not exist

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can be simplified as

catch (IOException | Exception e) {
    throw new OAuthStoreException("Failed to read PKCE code verifier from secure storage", e);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we cannot : there's compilation issue : Types in multi-catch must be disjoint: 'java. io. IOException' is a subclass of 'java. lang. Exception'

throw new OAuthStoreException("PKCE code verifier not found or expired for state or an "
+ "unexpected error reading from secure store: ", e);
}
}

public void deletePKCECodeVerifier(String provider, String state) throws OAuthStoreException {
String key = getPKCEKey(provider, state);
try {
secureStoreManager.delete(NamespaceId.SYSTEM.getNamespace(), key);
} catch (IOException e) {
throw new OAuthStoreException("Failed to delete PKCE code verifier from secure storage", e);
} catch (Exception e) {
Comment thread
vsethi09 marked this conversation as resolved.
// Ignore if not found

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if the error was not found.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NotFoundException and SecureKeyNotFoundException both are a part of cdap-common which not a dependency in this module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not safe to assume that any exception caught here would be of type NotFoundException or SecureKeyNotFoundException? There could be other kind of Exceptions here, like RuntimeException.

Is it safe to ignore the exception and not propagate it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For delete :
Yes , NotFoundException or SecureKeyNotFoundException is Safe to ignore here.

For any read operation error, we are catching IOexception.

But for any other exception yes we should catch ..

There is a UNCLEAN solution like :

catch (Exception e) {
      if (e.getClass().getName().endsWith("NotFoundException")) {
        // Ignore if already deleted
        return;
      }
      throw new OAuthStoreException("Failed to delete PKCE code verifier from secure storage", e);
    }

I don't think we should throw exception for NOT FOUND and break the flow.


Moreover , it's a delete op.

  • the PKCECodeVerifier anyway should be written with TTL , even if for some runtime it is missed, it will be cleaned up Eventually [ for GCP secret manager ]

Let me know your thoughts.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@

package io.cdap.cdap.datapipeline.oauth;

import javax.annotation.Nullable;

/**
* OAuth credential REST PUT request body.
*/
public class PutOAuthCredentialRequest {
private final String oneTimeCode;
private final String redirectURI;
@Nullable
private final String state;

public PutOAuthCredentialRequest(String oneTimeCode, String redirectURI) {
public PutOAuthCredentialRequest(String oneTimeCode, String redirectURI, @Nullable String state) {
this.oneTimeCode = oneTimeCode;
this.redirectURI = redirectURI;
this.state = state;
}

public String getOneTimeCode() {
Expand All @@ -36,4 +41,9 @@ public String getOneTimeCode() {
public String getRedirectURI() {
return redirectURI;
}

@Nullable
public String getState() {
return state;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,23 @@ public class PutOAuthProviderRequest {
private final String clientSecret;
private final OAuthProvider.CredentialEncodingStrategy strategy;
private final String userAgent;
private final AuthType authType;

public PutOAuthProviderRequest(
String loginURL,
String tokenRefreshURL,
String clientId,
String clientSecret,
OAuthProvider.CredentialEncodingStrategy strategy,
String userAgent) {
String userAgent,
AuthType authType) {
this.loginURL = loginURL;
this.tokenRefreshURL = tokenRefreshURL;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.strategy = strategy;
this.userAgent = userAgent;
this.authType = authType;
}

public String getLoginURL() {
Expand All @@ -66,4 +69,8 @@ public OAuthProvider.CredentialEncodingStrategy getCredentialEncodingStrategy()
public String getUserAgent() {
return userAgent;
}

public AuthType getAuthType() {
return authType;
}
}
Loading
Loading