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

AAD password on non windows #146

Merged
merged 7 commits into from
Feb 15, 2017
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
<version>0.9.7</version>
</dependency>

<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>adal4j</artifactId>
<version>1.1.3</version>
</dependency>

<!-- dependency for running tests -->
<dependency>
<groupId>junit</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.microsoft.sqlserver.jdbc;

import java.net.MalformedURLException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationException;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.sqlserver.jdbc.SQLServerConnection.ActiveDirectoryAuthentication;
import com.microsoft.sqlserver.jdbc.SQLServerConnection.SqlFedAuthInfo;

class SQLServerADAL4JUtils {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure you do not want public class? By default this will use package level visibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why would we want it to be public?


static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo,
String user,
String password,
String authenticationString) throws SQLServerException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
try {
AuthenticationContext context = new AuthenticationContext(fedAuthInfo.stsurl, false, executorService);
Future<AuthenticationResult> future = context.acquireToken(fedAuthInfo.spn, ActiveDirectoryAuthentication.jdbcFedauthClientId, user,
password, null);

AuthenticationResult authenticationResult = future.get();
SqlFedAuthToken fedAuthToken = new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate());

return fedAuthToken;
}
catch (MalformedURLException | InterruptedException e) {
throw new SQLServerException(e.getMessage(), null);
}
catch (ExecutionException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ADALExecution"));
Object[] msgArgs = {user, authenticationString};

// the cause error message uses \\n\\r which does not give correct format
// change it to \r\n to provide correct format
String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n");
AuthenticationException correctedAuthenticationException = new AuthenticationException(correctedErrorMessage);

// SQLServerException is caused by ExecutionException, which is caused by AuthenticationException
// to match the exception tree before error message correction
ExecutionException correctedExecutionException = new ExecutionException(correctedAuthenticationException);

throw new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
}
finally {
executorService.shutdown();
}
}
}
153 changes: 69 additions & 84 deletions src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import java.sql.Struct;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -137,40 +136,24 @@ class FederatedAuthenticationFeatureExtensionData {
}

class SqlFedAuthInfo {
private String spn;
private String stsurl;
String spn;
String stsurl;

@Override
public String toString() {
return "STSURL: " + stsurl + ", SPN: " + spn;
}
}

final class SqlFedAuthToken {
private final Date expiresOn;
private final String accessToken;


SqlFedAuthToken(final String accessToken,
final long expiresIn) {
this.accessToken = accessToken;

Date now = new Date();
now.setTime(now.getTime() + (expiresIn * 1000));
this.expiresOn = now;
}

Date getExpiresOnDate() {
return expiresOn;
}
}

private class ActiveDirectoryAuthentication {
private static final String jdbcFedauthClientId = "7f98cb04-cd1e-40df-9140-3bf7e2cea4db";
private static final String AdalGetAccessTokenFunctionName = "ADALGetAccessToken";
private static final int GetAccessTokenSuccess = 0;
private static final int GetAccessTokenInvalidGrant = 1;
private static final int GetAccessTokenTansisentError = 2;
private static final int GetAccessTokenOtherError = 3;
class ActiveDirectoryAuthentication {
static final String jdbcFedauthClientId = "7f98cb04-cd1e-40df-9140-3bf7e2cea4db";
Copy link
Contributor

@v-nisidh v-nisidh Feb 15, 2017

Choose a reason for hiding this comment

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

final static instance variables should be in CAPS

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

static final String AdalGetAccessTokenFunctionName = "ADALGetAccessToken";
static final int GetAccessTokenSuccess = 0;
static final int GetAccessTokenInvalidGrant = 1;
static final int GetAccessTokenTansisentError = 2;
static final int GetAccessTokenOtherError = 3;
}

/**
Expand Down Expand Up @@ -1270,8 +1253,8 @@ Connection connectInternal(Properties propsIn,
}

if ((!System.getProperty("os.name").toLowerCase().startsWith("windows"))
&& (!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString()))) {
throw new SQLServerException(SQLServerException.getErrString("R_FedAuthOnNonWindows"), null);
&& (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

in getFedAuthToken(..) we have similar check of ActiveDirectoryPassword. There we used authenticationString.trim(). Is there any possibility that we are getting authenticationString as null?

Copy link
Contributor Author

@xiangyushawn xiangyushawn Feb 15, 2017

Choose a reason for hiding this comment

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

no, the default is NotSpecified, it cannot be null. you can search for this line in the file and you will see:
sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue();

throw new SQLServerException(SQLServerException.getErrString("R_AADIntegratedOnNonWindows"), null);
}

sPropKey = SQLServerDriverStringProperty.WORKSTATION_ID.toString();
Expand Down Expand Up @@ -3441,6 +3424,8 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo,
}

private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException {
SqlFedAuthToken fedAuthToken = null;

// fedAuthInfo should not be null.
assert null != fedAuthInfo;

Expand All @@ -3450,83 +3435,83 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe
// No:of attempts, for tracing purposes, if we underwent retries.
int numberOfAttempts = 0;

FedAuthDllInfo dllInfo = null;

String user = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString());
String password = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString());

long expirationFileTime = 0;

while (true) {
numberOfAttempts++;

try {
if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) {
dllInfo = AuthenticationJNI.getAccessToken(user, password, fedAuthInfo.stsurl, fedAuthInfo.spn, clientConnectionId.toString(),
ActiveDirectoryAuthentication.jdbcFedauthClientId, expirationFileTime);
}
else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) {
dllInfo = AuthenticationJNI.getAccessTokenForWindowsIntegrated(fedAuthInfo.stsurl, fedAuthInfo.spn, clientConnectionId.toString(),
ActiveDirectoryAuthentication.jdbcFedauthClientId, expirationFileTime);
}

// AccessToken should not be null.
assert null != dllInfo.accessTokenBytes;
if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to have constant on left side while comparing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I totally agree, but there are a lot of places to change in the driver, maybe we can have a separate PR for this?

fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString);

// Break out of the retry loop in successful case.
break;
}
catch (DLLException adalException) {
else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) {
try {
long expirationFileTime = 0;
FedAuthDllInfo dllInfo = AuthenticationJNI.getAccessTokenForWindowsIntegrated(fedAuthInfo.stsurl, fedAuthInfo.spn,
clientConnectionId.toString(), ActiveDirectoryAuthentication.jdbcFedauthClientId, expirationFileTime);

// the sqljdbc_auth.dll return -1 for errorCategory, if unable to load the adalsql.dll
int errorCategory = adalException.GetCategory();
if (-1 == errorCategory) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnableLoadADALSqlDll"));
Object[] msgArgs = {Integer.toHexString(adalException.GetState())};
throw new SQLServerException(form.format(msgArgs), null);
}
// AccessToken should not be null.
assert null != dllInfo.accessTokenBytes;

int millisecondsRemaining = TimerRemaining(timerExpire);
if (ActiveDirectoryAuthentication.GetAccessTokenTansisentError != errorCategory || timerHasExpired(timerExpire)
|| (sleepInterval >= millisecondsRemaining)) {
byte[] accessTokenFromDLL = dllInfo.accessTokenBytes;

String errorStatus = Integer.toHexString(adalException.GetStatus());
String accessToken = new String(accessTokenFromDLL, UTF_16LE);

if (connectionlogger.isLoggable(Level.FINER)) {
connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken.AdalException category:" + errorCategory + " error: "
+ errorStatus);
fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn);

// Break out of the retry loop in successful case.
break;
}
catch (DLLException adalException) {

// the sqljdbc_auth.dll return -1 for errorCategory, if unable to load the adalsql.dll
int errorCategory = adalException.GetCategory();
if (-1 == errorCategory) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_UnableLoadADALSqlDll"));
Object[] msgArgs = {Integer.toHexString(adalException.GetState())};
throw new SQLServerException(form.format(msgArgs), null);
}

MessageFormat form1 = new MessageFormat(SQLServerException.getErrString("R_ADALAuthenticationMiddleErrorMessage"));
String errorCode = Integer.toHexString(adalException.GetStatus()).toUpperCase();
Object[] msgArgs1 = {errorCode, adalException.GetState()};
SQLServerException middleException = new SQLServerException(form1.format(msgArgs1), adalException);
int millisecondsRemaining = TimerRemaining(timerExpire);
if (ActiveDirectoryAuthentication.GetAccessTokenTansisentError != errorCategory || timerHasExpired(timerExpire)
|| (sleepInterval >= millisecondsRemaining)) {

MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ADALExecution"));
Object[] msgArgs = {user, authenticationString};
throw new SQLServerException(form.format(msgArgs), null, 0, middleException);
}
String errorStatus = Integer.toHexString(adalException.GetStatus());

if (connectionlogger.isLoggable(Level.FINER)) {
connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken sleeping: " + sleepInterval + " milliseconds.");
connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken remaining: " + millisecondsRemaining + " milliseconds.");
}
if (connectionlogger.isLoggable(Level.FINER)) {
connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken.AdalException category:" + errorCategory
+ " error: " + errorStatus);
}

try {
Thread.sleep(sleepInterval);
}
catch (InterruptedException e1) {
// continue if the thread is interrupted. This really should not happen.
}
sleepInterval = sleepInterval * 2;
}
}
MessageFormat form1 = new MessageFormat(SQLServerException.getErrString("R_ADALAuthenticationMiddleErrorMessage"));
String errorCode = Integer.toHexString(adalException.GetStatus()).toUpperCase();
Object[] msgArgs1 = {errorCode, adalException.GetState()};
SQLServerException middleException = new SQLServerException(form1.format(msgArgs1), adalException);

byte[] accessTokenFromDLL = dllInfo.accessTokenBytes;
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ADALExecution"));
Object[] msgArgs = {user, authenticationString};
throw new SQLServerException(form.format(msgArgs), null, 0, middleException);
}

String accessToken = new String(accessTokenFromDLL, UTF_16LE);
if (connectionlogger.isLoggable(Level.FINER)) {
connectionlogger.fine(toString() + " SQLServerConnection.getFedAuthToken sleeping: " + sleepInterval + " milliseconds.");
connectionlogger
.fine(toString() + " SQLServerConnection.getFedAuthToken remaining: " + millisecondsRemaining + " milliseconds.");
}

SqlFedAuthToken fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn);
try {
Thread.sleep(sleepInterval);
}
catch (InterruptedException e1) {
// continue if the thread is interrupted. This really should not happen.
}
sleepInterval = sleepInterval * 2;
}
}
}

return fedAuthToken;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ protected Object[][] getContents() {
{"R_TVPEmptyMetadata", "There are not enough fields in the Structured type. Structured types must have at least one field."},
{"R_TVPInvalidValue", "The value provided for Table-Valued Parameter {0} is not valid. Only SQLServerDataTable, ResultSet and ISQLServerDataRecord objects are supported."},
{"R_TVPInvalidColumnValue", "Input data is not in correct format."},
{"R_FedAuthOnNonWindows","Azure Active Directory is only supported on Windows operating systems."},
{"R_AADIntegratedOnNonWindows","ActiveDirectoryIntegrated is only supported on Windows operating systems."},
{"R_TVPSortOrdinalGreaterThanFieldCount", "The sort ordinal {0} on field {1} exceeds the total number of fields."},
{"R_TVPMissingSortOrderOrOrdinal", "The sort order and ordinal must either both be specified, or neither should be specified (SortOrder.Unspecified and -1). The values given were: order = {0}, ordinal = {1}."},
{"R_TVPDuplicateSortOrdinal", "The sort ordinal {0} was specified twice."},
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.microsoft.sqlserver.jdbc;

import java.util.Date;

class SqlFedAuthToken {
final Date expiresOn;
final String accessToken;

SqlFedAuthToken(final String accessToken,
final long expiresIn) {
this.accessToken = accessToken;

Date now = new Date();
now.setTime(now.getTime() + (expiresIn * 1000));
this.expiresOn = now;
}

SqlFedAuthToken(final String accessToken,
final Date expiresOn) {
this.accessToken = accessToken;
this.expiresOn = expiresOn;
}

Date getExpiresOnDate() {
return expiresOn;
}
}