Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JDBC sample app to show use of Auth Tokens to connect the database and get the tokens from endpoints #23

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c794200
Update OAuthSampleApp.java, Added refresh token api to get new access…
rvpanchal1202 Apr 17, 2024
1cccbff
Update OAuthSampleApp.java
rvpanchal1202 Apr 25, 2024
c49ab5d
Update OAuthSampleApp.java
rvpanchal1202 Apr 25, 2024
e48ed8c
Update OAuthSampleApp.java
rvpanchal1202 May 3, 2024
86f38e8
Create config.properties
rvpanchal1202 May 6, 2024
cb2a573
Update OAuthSampleApp.java
rvpanchal1202 May 6, 2024
9be665f
Update OAuthSampleApp.java
rvpanchal1202 May 6, 2024
43faac9
Update OAuthSampleApp.java
rvpanchal1202 May 6, 2024
7f9f3fe
Update OAuthSampleApp.java
rvpanchal1202 May 6, 2024
1f1dd1e
Update OAuthSampleApp.java
rvpanchal1202 May 6, 2024
43569a9
Update OAuthSampleApp.java
rvpanchal1202 May 7, 2024
e3fb283
Update config.properties
rvpanchal1202 May 7, 2024
24678f1
Update OAuthSampleApp.java
rvpanchal1202 May 8, 2024
e2b9639
Update OAuthSampleApp.java
rvpanchal1202 May 8, 2024
bc955dd
Update OAuthSampleApp.java
rvpanchal1202 May 8, 2024
222e8f2
Update OAuthSampleApp.java
rvpanchal1202 May 8, 2024
d467034
Update OAuthSampleApp.java
rvpanchal1202 May 8, 2024
19e7409
Update OAuthSampleApp.java
rvpanchal1202 May 10, 2024
26d633f
Update OAuthSampleApp.java
rvpanchal1202 May 10, 2024
932495e
Update OAuthSampleApp.java
rvpanchal1202 May 10, 2024
7241ce1
Update OAuthSampleApp.java
rvpanchal1202 May 15, 2024
e33d7a0
Update OAuthSampleApp.java
rvpanchal1202 May 16, 2024
cac73b9
Update OAuthSampleApp.java
rvpanchal1202 May 16, 2024
bdce33e
Update OAuthSampleApp.java
rvpanchal1202 May 16, 2024
58ec75a
Update OAuthSampleApp.java
rvpanchal1202 May 22, 2024
736b5e9
Update OAuthSampleApp.java
rvpanchal1202 May 28, 2024
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
363 changes: 285 additions & 78 deletions JDBC/OAuthSampleApp/src/main/java/OAuthSampleApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,91 +10,298 @@
// 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.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.SQLTransientConnectionException;
import java.sql.SQLInvalidAuthorizationSpecException;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.Callable;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

@Command(name = "OAuthSampleApp", mixinStandardHelpOptions = true, version = "OAuth sample app 1.0", description = "Connects to a Vertica database using OAuth")
public class OAuthSampleApp implements Callable<Integer> {

@Parameters(index = "0", description = "Host name")
private String host = "";

@Parameters(index = "1", description = "Database name")
private String dbName = "";

@Option(names = { "-p", "--port" }, description = "Port")
private String port = "5433";

@Option(names = { "-a", "--access-token" }, description = "Access token")
private String accessToken = "";

@Option(names = { "-r", "--refresh-token" }, description = "Refresh token")
private String refreshToken = "";

@Option(names = { "-s", "--client-secret" }, description = "Client Secret")
private String clientSecret = "";

private static Connection connectToDB(String host, String port, String dbName, String accessToken,
String refreshToken, String clientSecret) throws SQLException {
Properties jdbcOptions = new Properties();
jdbcOptions.put("oauthaccesstoken", accessToken);
jdbcOptions.put("oauthrefreshtoken", refreshToken);
jdbcOptions.put("oauthclientsecret", clientSecret);

return DriverManager.getConnection(
"jdbc:vertica://" + host + ":" + port + "/" + dbName, jdbcOptions);
}

private static ResultSet executeQuery(Connection conn) throws SQLException {
Statement stmt = conn.createStatement();
return stmt.executeQuery("SELECT user_id, user_name FROM USERS ORDER BY user_id");
}

private static void printResults(ResultSet rs) throws SQLException {
int rowIdx = 1;
while (rs.next()) {
System.out.println(rowIdx + ". " + rs.getString(1).trim() + " " + rs.getString(2).trim());
rowIdx++;
}
}

@Override
public Integer call() throws Exception {
try {
Connection conn = connectToDB(host, port, dbName, accessToken, refreshToken, clientSecret);
ResultSet rs = executeQuery(conn);
printResults(rs);
conn.close();
} catch (SQLTransientConnectionException connException) {
System.out.print("Network connection issue: ");
System.out.print(connException.getMessage());
System.out.println(" Try again later!");
} catch (SQLInvalidAuthorizationSpecException authException) {
System.out.print("Could not log into database: ");
System.out.print(authException.getMessage());
System.out.println(" Check the login credentials and try again.");
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}

public static void main(String[] args) {
int exitCode = new CommandLine(new OAuthSampleApp()).execute(args);
System.exit(exitCode);
}
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.net.URLEncoder;
import java.net.HttpURLConnection;
import java.net.URL;
import com.google.gson.JsonParser;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.vertica.jdbc.VerticaConnection;

public class OAuthSampleApp {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless it is just rendering poorly in github, the spacing in this class is all off. Indentation should simply be four spaces per indentation level and they should be actual spaces, not tabs.

Copy link
Author

Choose a reason for hiding this comment

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

Hi Danny,
This indentation formatting is done using the auto-indent feature(Source->Format) in Eclipse. So If we make any changes here in git for indentation, It will reflect wrong when we open the file in Eclipse or other IDE.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you are going to use the auto indent feature you need to update it to use four spaces instead of tabs, there should be an option for this. A space is a space, it's the same everywhere. A tab character may not render the same everywhere like we see here

Copy link
Author

@rvpanchal1202 rvpanchal1202 May 30, 2024

Choose a reason for hiding this comment

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

Ok. As per the last commit, All indentation was good and no issues observed. However, I have changed the indentation from the tabs to Spaces and committed the changes. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still seeing tabs in the PR. There is a setting in eclipse to use spaces instead of tabs when formatting code. If you have not enabled that, I suggest you change that and format the code again.

private static Properties prop;
private static Map<String, String> csProp = new HashMap<String, String>();
private static String OAUTH_ACCESS_TOKEN_VAR_STRING = "OAUTH_SAMPLE_ACCESS_TOKEN";
Copy link
Contributor

Choose a reason for hiding this comment

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

In java we declare constants as final so the constant properties here should be private static final

Constants are also generally grouped together and declared first in a class, so I would move these two constants to be the first properties defined in the class followed by a newline and then followed by the rest of the properties

Copy link
Author

Choose a reason for hiding this comment

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

Changed OAUTH_ACCESS_TOKEN_VAR_STRING, OAUTH_REFRESH_TOKEN_VAR_STRING to constant "private static final"

private static String OAUTH_REFRESH_TOKEN_VAR_STRING = "OAUTH_SAMPLE_REFRESH_TOKEN";
private static Connection vConnection;

// Get jdbc connection string from provided configuration
private static void getcsProp(String args) {
String[] entries = args.split(";");
for (String entry : entries) {
String[] pair = entry.split("=");
csProp.put(pair[0], pair[1]);
}
}

private static String getConnectionString() {
return "jdbc:vertica://" + csProp.get("Host") + ":" + csProp.get("Port") + "/" + csProp.get("Database")
+ "?user=" + csProp.get("User") + "&password=" + csProp.get("Password");
}

// Get the parameters from the connection String
private static String getParam(String paramName) {
return csProp.get(paramName);
}

// Add/Create Authentication record in database. Create User and grant the
// permissions for User
private static void SetUpDbForOAuth() throws Exception {
Copy link
Contributor

@DMickens DMickens May 23, 2024

Choose a reason for hiding this comment

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

In java convention for method names is to use camelCase. It's okay for the casing of the names in this sample to differ from the names in the ADO implementation in order to follow the convention. Please ensure all method names start with a lowercase letter and adhere to camelCase.

Copy link
Author

Choose a reason for hiding this comment

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

Changed all method names to camelCase.

String connectionString = getConnectionString();
vConnection = DriverManager.getConnection(connectionString);
String CONNECTION_STRING = prop.getProperty("ConnectionString");
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
String USER = prop.getProperty("User");
Copy link
Contributor

Choose a reason for hiding this comment

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

As these are not constant it should not be in all caps with underscore spacing. Please change these to be camelCase

Copy link
Author

Choose a reason for hiding this comment

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

Changed non constant vars to camelCase.

String CLIENT_ID = prop.getProperty("ClientId");
String CLIENT_SECRET = prop.getProperty("ClientSecret");
String DISCOVERY_URL = prop.getProperty("DiscoveryUrl");
Statement st = vConnection.createStatement();
st.execute("DROP USER IF EXISTS " + USER + " CASCADE;");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you put all of these commands into an array of strings, then iterate through the array and call st.execute() for each element of the array. I asked Kevin to make that change in the ADO sample previously as well. It makes it easier to see everything that is being executed and it's easy to modify if need be.

Copy link
Author

Choose a reason for hiding this comment

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

Added queries to a string array and executed the queries using iterating through the array.

st.execute("DROP AUTHENTICATION IF EXISTS v_oauth CASCADE;");
st.execute("CREATE AUTHENTICATION v_oauth METHOD 'oauth' LOCAL;");
st.execute("ALTER AUTHENTICATION v_oauth SET client_id= '" + CLIENT_ID + "';");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: either none should have spaces before the equal sign, or they all should. I would prefer that they all do, but lets make them consistent

Copy link
Author

Choose a reason for hiding this comment

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

Done.

st.execute("ALTER AUTHENTICATION v_oauth SET client_secret= '" + CLIENT_SECRET + "';");
st.execute("ALTER AUTHENTICATION v_oauth SET discovery_url = '" + DISCOVERY_URL + "';");
st.execute("CREATE USER " + USER + ";");
st.execute("GRANT AUTHENTICATION v_oauth TO " + USER + ";");
st.execute("GRANT ALL ON SCHEMA PUBLIC TO " + USER + ";");
st.close();
}

// Dispose the authentication record from database
private static void TearDown() {
try {
Statement st = vConnection.createStatement();
String USER = prop.getProperty("User");
Copy link
Contributor

Choose a reason for hiding this comment

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

not a constant, change to user

Copy link
Author

Choose a reason for hiding this comment

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

Done

st.executeUpdate("DROP USER IF EXISTS " + USER + " CASCADE");
st.executeUpdate("DROP AUTHENTICATION IF EXISTS v_oauth CASCADE");
vConnection.close();
} catch (Exception e) {
e.printStackTrace();
}
}

// Connect to Database using access Token
private static Connection connectToDB(String accessToken) throws SQLException {
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
Properties jdbcOptions = new Properties();
jdbcOptions.put("oauthaccesstoken", accessToken);
return DriverManager.getConnection(
"jdbc:vertica://" + getParam("Host") + ":" + getParam("Port") + "/" + getParam("Database"),
jdbcOptions);
}

// Test connection using access token and test database query result
private static void ConnectToDatabase() throws SQLException {
int connAttemptCount = 0;
while (connAttemptCount <= 1) {
try {
String accessToken = System.getProperty(OAUTH_ACCESS_TOKEN_VAR_STRING);
if (null == accessToken || accessToken.isEmpty()) {
throw new Exception("Access Token is not available.");
} else {
System.out.println("Attempting to connect with OAuth access token");
Connection conn = connectToDB(accessToken);
ResultSet rs = executeQuery(conn);
printResults(rs);
System.out.println("Query Executed. Exiting.");
break;
}
} catch (Exception ex) {
if (connAttemptCount > 0) {
break;
}
try {
GetTokensByRefreshGrant();
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e1) {
try {
GetTokensByPasswordGrant();
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e2) {
e2.printStackTrace();
}
}
++connAttemptCount;
}
}
}

// execute Simple query on database connection
private static ResultSet executeQuery(Connection conn) throws SQLException {
Statement stmt = conn.createStatement();
String query = "SELECT user_id, user_name FROM USERS ORDER BY user_id";
System.out.println("Executing query:" + query);
return stmt.executeQuery(query);
}

private static void printResults(ResultSet rs) throws SQLException {
int rowIdx = 1;
while (rs.next()) {
System.out.println(rowIdx + ". " + rs.getString(1).trim() + " " + rs.getString(2).trim());
rowIdx++;
}
}

// Get encoded URL from parameters
private static String getEncodedParamsString(Map<String, String> params) throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
result.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name()));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name()));
result.append("&");
}
result.setLength(result.length() - 1);
return result.toString();
}

// password grant logs into the IDP using credentials in app.config
public static void GetTokensByPasswordGrant() throws Exception {
Map<String, String> formData = new HashMap<String, String>();
formData.put("grant_type", "password");
formData.put("client_id", prop.getProperty("ClientId"));
formData.put("client_secret", prop.getProperty("ClientSecret"));
formData.put("username", prop.getProperty("User"));
formData.put("password", prop.getProperty("Password"));
GetAndSetTokens(formData);
}

// refresh grant uses the refresh token to get the new access and refresh token
public static void GetTokensByRefreshGrant() throws Exception {
Map<String, String> formData = new HashMap<String, String>();
formData.put("grant_type", "refresh_token");
formData.put("client_id", prop.getProperty("ClientId"));
formData.put("client_secret", prop.getProperty("ClientSecret"));
formData.put("refresh_token", System.getProperty(OAUTH_REFRESH_TOKEN_VAR_STRING));
GetAndSetTokens(formData);
}

// read result from Buffered input stream
private static ByteArrayOutputStream readResult(BufferedInputStream in, ByteArrayOutputStream buf) {
try {
for (int result = in.read(); result != -1; result = in.read()) {
buf.write((byte) result);
}
} catch (Exception e) {
e.printStackTrace();
}
return buf;
}

// get and set tokens from IDP
private static void GetAndSetTokens(Map<String, String> formData) throws Exception {
try {
String postOpts = getEncodedParamsString(formData);
byte[] postData = postOpts.getBytes("UTF-8");
int postDataLength = postData.length;
URL url = new URL(prop.getProperty("TokenUrl"));
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Content-Length", Integer.toString(postDataLength));
connection.setRequestProperty("Accept", "application/json");
connection.getOutputStream().write(postData);
BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
ByteArrayOutputStream buf = new ByteArrayOutputStream();
readResult(in, buf);
String res = buf.toString("UTF-8");
JsonElement jElement = JsonParser.parseString(res);
JsonObject jObject = jElement.getAsJsonObject();
// set Tokens as System Properties - new values to access_token and
// refresh_token
String accessToken = jObject.has("access_token") ? jObject.get("access_token").getAsString() : null;
String refreshToken = jObject.has("refresh_token") ? jObject.get("refresh_token").getAsString() : null;
if (null == accessToken || null == refreshToken) {
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
throw new Exception(
"Access/refresh token is null, Please verify the config.properties for proper inputs.");
}
System.setProperty(OAUTH_ACCESS_TOKEN_VAR_STRING, accessToken);
System.setProperty(OAUTH_REFRESH_TOKEN_VAR_STRING, refreshToken);
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
} catch (UnsupportedEncodingException uee) {
uee.printStackTrace();
} catch (Exception e) {
String res = "";
try {
BufferedInputStream in = new BufferedInputStream(connection.getErrorStream());
ByteArrayOutputStream buf = new ByteArrayOutputStream();
readResult(in, buf);
res = buf.toString("UTF-8");
} catch (Exception ex) {
// Skip when this happens, but print the error.
ex.printStackTrace();
}
throw e;
} finally {
connection.disconnect();
}
} catch (IOException unreportedex) {
unreportedex.printStackTrace();
}
}

// If there is no access token, Get it using password grant
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
private static void EnsureAccessToken() throws Exception {
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
try {
String accessToken = System.getenv(OAUTH_ACCESS_TOKEN_VAR_STRING);
Copy link
Contributor

Choose a reason for hiding this comment

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

System.getenv will not throw an exception, just return null. So these should be moved outside of the try block

Copy link
Author

Choose a reason for hiding this comment

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

Moved System.getenv outside of try block

if (null == accessToken || accessToken.isEmpty()) {
// Obtain first access token and refresh Tokens
GetTokensByPasswordGrant();
} else {
System.setProperty(OAUTH_ACCESS_TOKEN_VAR_STRING, accessToken);
}
} catch (Exception e) {
throw e;
}
}

// load the configuration properties
public static void loadProperties() {
prop = new Properties();
try (InputStream input = new FileInputStream("src/main/java/config.properties")) {
// load the properties file
prop.load(input);
// Get the connectionString parameters from properties prop
getcsProp(prop.getProperty("ConnectionString"));
} catch (Exception ex) {
ex.printStackTrace();
}
}

// main function, Call starts from here
public static void main(String[] args) {
try {
loadProperties();
SetUpDbForOAuth();
EnsureAccessToken();
ConnectToDatabase();
} catch (SQLTransientConnectionException connException) {
kevinkarch88 marked this conversation as resolved.
Show resolved Hide resolved
connException.printStackTrace();
} catch (SQLInvalidAuthorizationSpecException authException) {
authException.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception unreportedEx) {
unreportedEx.printStackTrace();
} finally {
TearDown();
}
System.exit(0);
}
}
7 changes: 7 additions & 0 deletions JDBC/OAuthSampleApp/src/main/java/config.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
User=
Password=
ClientId=
ClientSecret=
TokenUrl=
DiscoveryUrl=
ConnectionString=Host=<Database Host>;Port=<Database Port>;Database=<DBName>;User=<username>;Password=<Password>;