tediousjs / tedious

Node TDS module for connecting to SQL Server databases.

Home Page:http://tediousjs.github.io/tedious/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to connect to Azure SQL using Azure AD Credentials

jamesderrick opened this issue · comments

I have followed every guide I can find but cannot connect to my company's Azure SQL DB. I believe everything in the cloud is working fine as I'm able to connect to the database using Azure Data Studio without issue.

I'm using the latest version of tedious v14.3
And using Node v16.13

My config looks like this...

const config = {
  authentication: {
    options: {
      userName: '<my ad username>',
      password: '<my ad password>' 
    },
    type: 'azure-active-directory-password' 
  },
  server: '<azure-server>.database.windows.net'
  options: {
    database: '<azure-database>',
    encrypt: true
  }
}

I'm getting a deprecation warning regarding not having a clientId, but I shouldn't need one, and also don't think the server has one in it's current state.
And I'm receiving the message "Security token could not be authenticated or authorized"

I've done some experimenting, and with my current config it works with tedious v12.3 but nothing newer than that. So I'm either missing something crucial or something has broken

Hi @jamesderrick, for the clientId, on the tedious side, we used to hardcode a client from a registered application that we set up internally. We recently found out that is not a good practice to do it this way, the best practice should be: users set up their own registered applications and grab their own client id and pass that into tedious via a connection config we provided call clinetId. That is why we have a warning message here to notify users ahead of about this behavior change. There is a doc from the Microsoft site for registered app set up. During this transition period, we still allow users to use the hardcoded clinetId for now, but it will be removed entirely in future major releases.

For the "Security token could not be authenticated or authorized" error, it should be caused that you are missing a tendenID from the connection config. If you do not provide a tenantId here, a default id "common" will be used here, which does not work with your current server-side setup.

You can provide you tenantId like this:

const config = {
    authentication: {
        options: {
            userName: 'userID', // update me
            password: '' ,// update me
            tenantId: '< tenantId >' ,// update me
        },
        type: 'azure-active-directory-password'
    },
    server: 'ServerName', // update me
    options: {
        database: 'DBName', //update me
        encrypt: true
}

Thank you, I will give that a try.
If I'm using the mssql node package, do you know if that will also take a tenantId? Given that that depends on tedious I've run into the same issue and have reverted to using an older version of that for the time being

I can confirm mssql does accept tenantId, however using the latest version of that (v8.0.2), authentication is unsuccessful. I get the same "Security token could not be authenticated or authorized" error. Just let me know if I should raise another issue on the mssql GitHub page, though it seems like I'm missing things rather than it being a bug

I also checked mssql is still on tedious 14.0.0, and the tenantId change is in 14.3.0. However, during the mean time, you can try change the key name from tenantId to domain. That is the old name for accepting tenantId in the past, but we will that is kind misleading for users, so we made a change in tedious version 14.3.0.

const config = {
    authentication: {
        options: {
            userName: 'userID', // update me
            password: '' ,// update me
            **domain**: '< tenantId >' ,// update me
        },
        type: 'azure-active-directory-password'
    },
    server: 'ServerName', // update me
    options: {
        database: 'DBName', //update me
        encrypt: true
}

Also, it will be great if you could also post an issue on mssql side to remind them for updating their tedious dependency to accommodate the latest changes.

Testing tenantId in tedious will have to be a task for tomorrow as I can't get back into my work vm today. However, I've tried using the latest mssql with tenantId and domain (I get a deprecation warning if I use domain) and neither one works, both give the same authorisation error as tedious. I have to go back to mssql v7.1 for Azure AD auth to work for me with my current config (with tenantId included)

Targeting tedious code directly, using the latest version v14.3, with tenantId included in the config, I still get the error message. The code is 'EFEDAUTH' and message is the same as previously mentioned.
I moved some bits around, so I'll add my config again just to make certain I've not messed that up

const config = {
    authentication: {
        type: 'azure-active-directory-password'
        options: {
            userName: '<azure ad username>',
            password: '<azure ad password>' ,
            tenantId: '< azure ad tenantId >' ,
        },
    },
    server: '<azure-server>.database.windows.net',
    database: '<azure database>',
    options: {
        encrypt: true
    }
}

I've tried database in options and outside of it and it doesn't seem to make a difference

Hi @jamesderrick, I have tried your configuration settings and it works for me on my end, so the configuration should be fine. There is one more thing that you could try on your side for additional information related to this error. You could add a console log to the tedious side code which directly logs the original error during the run time. The original error has more detail related to why the server-side cannot grant this token request.

How to steps:

  • First, you need to locate the connection.ts file under the tedious project. Either the require or import statement on top of your script should point to the location.
  • Find a function called getToken under connection.ts
  • Add a line console.log(err); after if(err){
    This si what the modification should looks like:
getToken((err, token) => {
    if (err) {
      this.loginError = new _errors.ConnectionError('Security token could not be authenticated or authorized.', 'EFEDAUTH');
      console.log(err);
      this.emit('connect', this.loginError);
      this.transitionTo(this.STATE.FINAL);
      return;
}

This should allow you to see the original err in the terminal when you execute your script.

We are currently working on a PR on our side to handle this error better, rather than just swallow the original err and give this generic message: "Security token could not be authenticated or authorized" like what we currently did.

The two common messages that we currently run into before are:
"Due to a configuration change made by your administrator, or because you moved to a new location, you must enroll in multi-factor authentication to access ''." This happens when we run the test outside an Azure environment like an Azure VM.

"The grant type is not supported over the /common or /consumers endpoints. Please use the /organizations or tenant-specific endpoint." This happens when we did not provide a proper tenantId.

If you are able to retrieve the original error message, then we can look into hat see what is causing your connection problem.

I've added in the extra log message, and at the moment I'm getting "AADSTS50126: Error validating credentials due to invalid username or password", which is strange because I'm definitely using the right credentials. These credentials work fine with an older version of Tedious, so I'm confident the creds are correct

I also got a Trace ID and Correlation ID from the extra console log, so I'm chasing our cloud admin up to see if we can get more information around the error

I've also checked this document as guide. It doesn't mention Azure AD authentication so I'm finding it quite hard to find an up-to-date document for how to use AAD with NodeJS
Microsoft Guide

Tedious migrated from using adal-node in Tedious 12.3 to msal-node from Tedious 13 onwards.

Not sure if this is the same issue you're running into, but there was an old issue where the user was getting the same error message when migrating to msal-node because the user was on an older version of AD FS that is not supported by msal.

AzureAD/microsoft-authentication-library-for-js#3991

We will continue to look into this.

Just found a link for checking AD FS compatibility. @jamesderrick you can take a look as well, see if that helps.

Think the AD that our company uses is simply Azure AD with a single tenant. Don’t think we use ADFS

Ok, I take back what I said, we do indeed have federation enabled and the domain is definitely verified, however, I don't have the access permissions to find out any more than that. Will ask within my company but information is never quick to get. I can, however, confirm that AD sign ins for everything (azure, office, outlook etc.) work just fine, only issue I've come across is when trying to make it work with Tedious.

After further discussion with the team, it may also be the case that MSAL is not escaping special characters in the password correctly. If your current password has special characters, is it possible to temporarily try a password that avoids most special characters (<>&%', etc) but still meets strong password requirements, e.g. something like "Just-4-Testing-Tedious"?

Hi, I appreciate the response, but the password I’ve been trying already does not contain any special characters

Hi @jamesderrick, for the clientId, on the tedious side, we used to hardcode a client from a registered application that we set up internally. We recently found out that is not a good practice to do it this way, the best practice should be: users set up their own registered applications and grab their own client id and pass that into tedious via a connection config we provided call clinetId. That is why we have a warning message here to notify users ahead of about this behavior change. There is a doc from the Microsoft site for registered app set up. During this transition period, we still allow users to use the hardcoded clinetId for now, but it will be removed entirely in future major releases.

For the "Security token could not be authenticated or authorized" error, it should be caused that you are missing a tendenID from the connection config. If you do not provide a tenantId here, a default id "common" will be used here, which does not work with your current server-side setup.

You can provide you tenantId like this:

const config = {
    authentication: {
        options: {
            userName: 'userID', // update me
            password: '' ,// update me
            tenantId: '< tenantId >' ,// update me
        },
        type: 'azure-active-directory-password'
    },
    server: 'ServerName', // update me
    options: {
        database: 'DBName', //update me
        encrypt: true
}

This needs to be added to the docs ASAP. I was struggling for hours yesterday trying to figure out why my credentials worked in SSMS but not in my app, and just adding the tenantId immediately resolved this issue!

Hi @nihonjinboy85 , sorry to hear that this still causing issues for users. the most recent version of our GitHub io doc already includes this and we are making a PR to explain a bit more on why tenantID is optional but needed in most cases. Hopefully, the doc will help people clear up on AAD password authentication type in the future.
image

Hi @jamesderrick, for the clientId, on the tedious side, we used to hardcode a client from a registered application that we set up internally. We recently found out that is not a good practice to do it this way, the best practice should be: users set up their own registered applications and grab their own client id and pass that into tedious via a connection config we provided call clinetId. That is why we have a warning message here to notify users ahead of about this behavior change. There is a doc from the Microsoft site for registered app set up. During this transition period, we still allow users to use the hardcoded clinetId for now, but it will be removed entirely in future major releases.
For the "Security token could not be authenticated or authorized" error, it should be caused that you are missing a tendenID from the connection config. If you do not provide a tenantId here, a default id "common" will be used here, which does not work with your current server-side setup.
You can provide you tenantId like this:

const config = {
    authentication: {
        options: {
            userName: 'userID', // update me
            password: '' ,// update me
            tenantId: '< tenantId >' ,// update me
        },
        type: 'azure-active-directory-password'
    },
    server: 'ServerName', // update me
    options: {
        database: 'DBName', //update me
        encrypt: true
}

This needs to be added to the docs ASAP. I was struggling for hours yesterday trying to figure out why my credentials worked in SSMS but not in my app, and just adding the tenantId immediately resolved this issue!

Thanks but we’ve already tried using the tenantId but we still get the error

Hi @nihonjinboy85 , sorry to hear that this still causing issues for users. the most recent version of our GitHub io doc already includes this and we are making a PR to explain a bit more on why tenantID is optional but needed in most cases. Hopefully, the doc will help people clear up on AAD password authentication type in the future. image

Great! I was probably looking at some other Microsoft docs that omit that parameter.

On that note, should I consider the github.io doc as the source of truth when utilizing tedious (via the mssql npm package in my case)? For example, I'm working on implementing managed identity auth for my API using azure-active-directory-msi-app-service. When I look at the github.io docs, it shows the config as:

"authentication": {
  "type": 'azure-active-directory-msi-app-service',
  "options": {
    "clientId": value,
    "msiEndpoint": value,
    "msiSecret": value
  }
}

However, when I look at this tutorial, it shows the config as (without any parameters):

"authentication": {
  "type": 'azure-active-directory-msi-app-service'
}

Which one is correct? Also, is there a way to use azure-active-directory-msi-app-service and/or azure-active-directory-password without creating an app registration (preferably using the DefaultAzureCredential provided by the @azure/identity npm package)?

Hi @jamesderrick, for the clientId, on the tedious side, we used to hardcode a client from a registered application that we set up internally. We recently found out that is not a good practice to do it this way, the best practice should be: users set up their own registered applications and grab their own client id and pass that into tedious via a connection config we provided call clinetId. That is why we have a warning message here to notify users ahead of about this behavior change. There is a doc from the Microsoft site for registered app set up. During this transition period, we still allow users to use the hardcoded clinetId for now, but it will be removed entirely in future major releases.
For the "Security token could not be authenticated or authorized" error, it should be caused that you are missing a tendenID from the connection config. If you do not provide a tenantId here, a default id "common" will be used here, which does not work with your current server-side setup.
You can provide you tenantId like this:

const config = {
    authentication: {
        options: {
            userName: 'userID', // update me
            password: '' ,// update me
            tenantId: '< tenantId >' ,// update me
        },
        type: 'azure-active-directory-password'
    },
    server: 'ServerName', // update me
    options: {
        database: 'DBName', //update me
        encrypt: true
}

This needs to be added to the docs ASAP. I was struggling for hours yesterday trying to figure out why my credentials worked in SSMS but not in my app, and just adding the tenantId immediately resolved this issue!

Thanks but we’ve already tried using the tenantId but we still get the error

Sorry to hear that. It fixed my issue, but obviously your specific configuration is different than mine. I'll defer to the package contributors to help you out. Good luck!

Hi @nihonjinboy85 , We will always try to keep the github.io page for tedious up to date, so this documentation should be users' first choice for utilizing tedious. In terms of the other question, I remember that we recently merged a change that allows users to use DefaultAzureCredential provided by the @azure/identity. I will double-check to confirm this, and the documentation will be updated soon as well.

@nihonjinboy85 , just checked, the PR is approved, but not merged yet. We will try to get it merged, and I will let you know when it happens. The PR number is #1365.

Hi @nihonjinboy85 , We will always try to keep the github.io page for tedious up to date, so this documentation should be users' first choice for utilizing tedious. In terms of the other question, I remember that we recently merged a change that allows users to use DefaultAzureCredential provided by the @azure/identity. I will double-check to confirm this, and the documentation will be updated soon as well.

That's wonderful, @MichaelSun90 ! If you could shoot me a link to either once confirmed, I'd really appreciate it. For now, I'm using azure-active-directory-password and provide the corresponding options in dev (if (process.env.NODE_ENV === 'development')) and using azure-active-directory-msi-app-service otherwise.

Thanks!

I would definitely like to use DefaultAzureCredential as soon as possible too, to work around some of these more complicated issues.

Yeah DefaultAzureCredential would be great. I've actually been using that already but to generate an access token and using the access token to connect to the Azure SQL

const credential = new DefaultAzureCredential()
const {token} = await credential.getToken("https://database.windows.net/.default")

const config = {
...
  authentication: {
    type: 'azure-active-directory-access-token',
    options: {
      token: token
    }
  }
...
}

@jamesderrick ia azure-active-directory-access-token working for you?

Yeah I believe so. I have to sign in to my azure account in visual studio code using the Microsoft Azure extension then DefaultAzureCredential can authenticate as me and generate a valid token. And that token authenticates with Azure SQL just fine.
Has only helped me locally. The same thing doesn't work in Azure but that could easily be how things have been architected by someone else in my team. And I don't get visibility of that.

@jamesderrick nice, I was asking as I spent two days on this #1418 and still no success

@jamesderrick Thank you SO much for sharing this hack! It worked for me as well, so now I don't have to check the env to decide whether to use azure-active-directory-password or azure-active-directory-msi-app-service.

We've just merged PR #1365 which adds support for DefaultAzureCredential, but it hasn't been released to NPM yet. In the meantime, you could continue using azure-active-directory-access-token as a workaround.

Also, could give this comment a look to see if that solves your issue.

type: azure-active-directory-default (DefaultAzureCredential) has been released on Tedious version 14.4.0

That's awesome, @mShan0 ! I'm currently using the mssql package to interact with our SQL server & database, so any idea when that team will be able to incorporate the updated Tedious driver support into their package? The current version of mssql (v8.0.2) uses tedious v14.0.0. If you don't have visibility into that team's roadmap, I'm happy to create an issue on their repo to request support for tedious v14.4.0 once released (NPM shows tedious v14.3.0 as the current version).

Hi @nihonjinboy85, unfortunately I don't know when node-mssql will be updated since we don't do support for it.

No worries, @mShan0 . Once Tedious v14.4.0 is available on NPM, I'll ask them to implement support for it. Thanks for getting the new feature merged... I'm really excited to be able to start using it once available through mssql!

UPDATE

Since refactoring my API to use azure-active-directory-access-token as mentioned in @jamesderrick 's workaround, I'm encountering a new issue. The token I'm creating through @azure/identity expires after 24 hours (all docs say 1 hour, but I've logged the expiresOnTimestamp prop on server startup and it's definitely 24 hours). The way I've configured my requests are such that the API checks to see if the token has expired (i.e. tokenObj.expiresOnTimestamp < Date.now()) and, if so, it renews the token, recreates the config with the new token, and then reestablishes the SQL DB connection. Unfortunately, this isn't working. Instead, after the token expires, the token IS refreshed, but the SQL DB starts refusing requests and throwing an ELOGIN ConnectionError.

I believe the reason for this is that since the existing connection pool is never explicitly closed, when my API send a new config object with the updated token, the existing connection pool is returned instead of a new one being created. This assumption is supported by this statement from the README:

NB: It's important to note that there can only be one global connection pool connected at a time. Providing a different connection config to the connect() function will not create a new connection if it is already connected.

My question is: should I close the connection pool before attempting to reestablish the connection, or am I approaching this problem incorrectly? Code below for context.

P.S. - I've also been discussing this with @azure/identity on this thread, but it seems clear now that the token is refreshing as expected, so this is a SQL connection issue, not an AAD issue.

const configureSQL = token => {
  const config = {
    authentication: {
      type: 'azure-active-directory-access-token',
      options: {
        token: token
      }
    },
    server: process.env.DB_SERVER,
    database: process.env.DB_NAME,
    options: {
      encrypt: true,
      enableArithAbort: true
    },
    pool: {
      max: 10,
      min: 0,
      idleTimeoutMillis: 30000
    }
  };

  return config;
};

app.locals.credential = new DefaultAzureCredential();

app.get('/', async (req, res) => {
  try {
    if (req.app.locals.tokenObj.expiresOnTimestamp < Date.now()) {
      req.app.locals.tokenObj = await req.app.locals.credential.getToken(
        'https://database.windows.net/.default'
      );
      const config = configureSQL(req.app.locals.tokenObj.token);
      req.app.locals.db = await sql.connect(config);
    }
    const result = await req.app.locals.db.query('SELECT * FROM Candidate');
    res.send(result.recordset);
  } catch (err) {
    res.status(500).send(err);
  }
});

app.listen(process.env.PORT, async () => {
  app.locals.tokenObj = await app.locals.credential.getToken(
    'https://database.windows.net/.default'
  );
  const config = configureSQL(app.locals.tokenObj.token);
  sql.connect(config).then(pool => {
    app.locals.db = pool;

    logger.info(`API listening on port ${process.env.PORT}...`);
  });
});

Hi @nihonjinboy85 , I think you assumption is correct that existing connection pool is returned instead of a new one being created which causing that token update does not worked as you expected. In my opinion, close the exist pool, and open a new one will be a proper solution, but I am not an expert on connection pool. You can try post you question on mssql side issue board, and see if anyone has a better solution for resolving this.

Thanks for confirming my assumptions, @MichaelSun90 . Yes, I've updated my API to now completely close the SQL connection, refresh the token, and then re-establish the connection whenever the token has expired and a new request is being processed. Also, I've lined up my next update in the queue which now utilizes azure-active-directory-default now that I've confirmed support with the mssql team. This refactor completely removes the need for token handling in my use case and is such a great addition to the package, so thank you for helping to get that feature pushed through!

Take care!

Thanks for let us know. I will close this one for now. If anything come up, we can either reopen this one or chat in a new issue.

Sounds good, @MichaelSun90 . BTW, I mentioned you and @mShan0 in a recent blog article I wrote documenting the configuration process: https://blog.richardcarrigan.dev/azure-sql-node-managed-id-part2, so feel free to check that out. Thanks again!