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

rds$build_auth_token not working #324

Open
Drwhit opened this issue Sep 5, 2020 · 28 comments
Open

rds$build_auth_token not working #324

Drwhit opened this issue Sep 5, 2020 · 28 comments
Labels
bug 🐞 Something isn't working

Comments

@Drwhit
Copy link

Drwhit commented Sep 5, 2020

Thanks for your work on this package, looks like it will come in very handy.

I'm a beginner at aws authentication especially related to databases, so it could definitely be user error, but it appears to me that the rds$build_auth_token function isn't working.

When I obtain the token using the this function, I am unable to connect to the database (i've replaced sensitive information with "xxxxxxxx":

token <- rds$build_auth_token(endpoint = "xxxxxxxx.us-west-2.rds.amazonaws.com:5432", region = "us-west-2", user = "xxxxxxxx")
pool <- pool::dbPool(drv = RPostgres::Postgres(), dbname="xxxxxxxx", host="xxxxxxxx.us-west-2.rds.amazonaws.com", user= "xxxxxxxx", password=token, bigint = "numeric")

Error in connection_create(names(opts), as.vector(opts)) : FATAL: PAM authentication failed for user "xxxxxxxx" FATAL: pg_hba.conf rejects connection for host "xxxxxxxx", user "xxxxxxxx", database "xxxxxxxx", SSL off

However, if I obtain the token using the following command, I am able to connect:
systoken <- system("aws rds generate-db-auth-token --hostname xxxxxxxx.us-west-2.rds.amazonaws.com --port 5432 --username xxxxxxxx")
pool <- pool::dbPool(drv = RPostgres::Postgres(), dbname="xxxxxxxx", host="xxxxxxxx.us-west-2.rds.amazonaws.com", user= "xxxxxxxx", password=systoken, bigint = "numeric")

Here is some system info:

sessionInfo()
R version 3.5.2 (2018-12-20)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS 10.15.5

Matrix products: default
BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

@davidkretch
Copy link
Member

Thank you for the bug report! We'll check it out in the next week.

@davidkretch davidkretch added the bug 🐞 Something isn't working label Sep 6, 2020
@davidkretch
Copy link
Member

davidkretch commented Sep 12, 2020

Hello @Drwhit, unfortunately I was not able to reproduce the issue on Linux or Windows in us-east-2 using rds$build_auth_token and pool::dbPool. I don't have a Mac to test with but doubt it is platform specific. I don't have many ideas of what it could be, but possibly do you have more than one profile or set of credentials in your ~/.aws/config or ~/.aws/credentials?

@Drwhit
Copy link
Author

Drwhit commented Sep 15, 2020 via email

@davidkretch
Copy link
Member

Cool, thank you. I'll try to set something like that up.

@davidkretch davidkretch self-assigned this Nov 27, 2020
@davidkretch davidkretch removed their assignment Jan 25, 2021
@tnederlof
Copy link

I am having the same issue (although I am using the odbc package to connect instead of pool with the same error PAM authentication failed for user: XXXX.

I do get a token back but it seems not to work.

In my case I am using AWS_WEB_IDENTITY_TOKEN_FILE not secret/key credentials. However other paws actions such as S3 access work fine as expected.

Here is the code I am trying and is giving the error:

library(odbc)
library(DBI)
library(paws)

rds <- paws::rds()

token <- rds$build_auth_token(endpoint = "XXXXXXX.us-east-2.rds.amazonaws.com", region = "us-east-2", user = "user1") 

con <- DBI::dbConnect(odbc::odbc(),
                      Driver   = "PostgreSQL",
                      Server   = "XXXXXXX.us-east-2.rds.amazonaws.com",
                      Database = "demo_db",
                      UID      = "user1",
                      PWD      = token,
                      Port     = 5432
                      )

Everything works as expected if I use the AWS cli or use boto3.

For example, this code calling boto3 from reticulate does work:

library(odbc)
library(DBI)
library(reticulate)

boto3 <- import("boto3")
client = boto3$client('rds', region_name="us-east-2")
token = client$generate_db_auth_token(DBHostname='XXXXXXX.us-east-2.rds.amazonaws.com', Port=5432L,DBUsername="user1", Region="us-east-2")

con <- DBI::dbConnect(odbc::odbc(),
                      Driver   = "PostgreSQL",
                      Server   = "XXXXXXX.us-east-2.rds.amazonaws.com",
                      Database = "demo_db",
                      UID      = "user1",
                      PWD      = token,
                      Port     = 5432,
                      )

@atheriel do you have ideas?

@DyfanJones
Copy link
Member

@tnederlof thanks for raising this issue. I will revisit this before next paws.common release

@tnederlof
Copy link

Thank you for taking a look @DyfanJones let me know if there is more info I can provide to help, I have some envs on my side to reproduce.

@atheriel
Copy link

atheriel commented Jan 9, 2025

I notice that the code provided above has this line:

token <- rds$build_auth_token(endpoint = "XXXXXXX.us-east-2.rds.amazonaws.com", region = "us-east-2", user = "user1") 

but looking at the test suite here it seems like you need to add the port to the endpoint:

test_that("rds_build_auth_token", {
actual <- rds_build_auth_token(
"prod-instance.us-east-1.rds.amazonaws.com:3306",
"us-west-2",
"mysqlUser",
creds = list(
access_key_id = "AKIA",
secret_access_key = "SECRET",
session_token = "SESSION"
)
)
expect_match(actual, "^prod-instance\\.us-east-1\\.rds\\.amazonaws\\.com:3306/\\?Action=connect.*?DBUser=mysqlUser.*")
})

Maybe that's the source of the issue? I'm also a little confused about why you need to provide region when it appears in the endpoint string -- what happens when (if?) these are inconsistent?

@tnederlof
Copy link

I did try the port in the endpoint and still got the same error when I used the token to connect

@DyfanJones
Copy link
Member

I wouldn't expect a port to cause any issues. I have recently been working on the urlparse migrating it to cpp and the port is always attached to the host :)

@DyfanJones
Copy link
Member

I believe it is losing it credentials somehow. I am prototyping some possible replacements

@atheriel @tnederlof Do you mind testing out this prototype method. I want to make sure it keeps its credentials.

presign <- function(request, default = paws.common:::v1_sign_request_handler) {
  # build request
  request <- paws.common:::build(request)
  signer <- switch(request$config[["signature_version"]],
    "v1" = paws.common:::v1_sign_request_handler,
    "s3" = paws.common:::s3_sign_request_handler,
    "s3v4" = paws.common:::s3v4_sign_request_handler,
    "v4" = paws.common:::v4_sign_request_handler,
    default
  )

  # sign request
  signer(request)
}

rds_build_auth_token_v2 <- function(DBHostname, Port, DBUsername, Region=NULL) {
  op <- paws.common:::new_operation(
    name = "BuildAuthTokenV2", http_method = "GET",
    http_path = "/", host_prefix = "", paginator = list(), stream_api = F
  )
  input <- list(
    Action = "connect",
    DBUser = DBUsername
  )
  output <- list()

  config <- paws.common::get_config()
  
  if (!startsWith(DBHostname, "https://")) DBHostname <- paste0("https://", DBHostname)
  config$endpoint <- paste(DBHostname, Port, sep = ":")

  svc <- paws.database:::.rds$service(config, op)

  # create new request
  request <- paws.common::new_request(svc, op, input, output)
  request$expire_time <- 900

  request <- presign(request, paws.common:::v4_sign_request_handler)
  request$http_request$url$raw_query <- sub("&Version=[0-9]{,4}-[0-9]{,2}-[0-9]{,2}", "", request$http_request$url$raw_query)

  url <- paws.common:::build_url(request$http_request$url)
  return(substr(url, 9, nchar(url)))
}
library(paws)
client = rds()

# add prototype method to client
client$build_auth_token_v2 <- rds_build_auth_token_v2

client$build_auth_token_v2(
  DBHostname='XXXXXXX.us-east-2.rds.amazonaws.com',
  Port = 5432,
  DBUsername="user1"
)

@tnederlof
Copy link

@DyfanJones, unfortunately, I still get the same error on my end using that resulting token. Happy to test anything else that helps. I have a boto3 example running with reticulate to make sure I can keep connecting that way.

@DyfanJones
Copy link
Member

@tnederlof thanks, are you ok to run some prototype functions for me? I currently don't have an equivalent environment to test this out.

@tnederlof
Copy link

@DyfanJones yes Ill keep my environment around, happy to test!

@DyfanJones
Copy link
Member

DyfanJones commented Jan 15, 2025

@tnederlof can you try the following prototype function?

rds_build_auth_token_v2 <- function(DBHostname, Port, DBUsername, Region=NULL) {
  op <- paws.common:::new_operation(
    name = "rds-db", http_method = "GET",
    http_path = "/", host_prefix = "", paginator = list(), stream_api = F
  )
  input <- list(
    Action = "connect",
    DBUser = DBUsername
  )
  output <- list()

  config <- paws.common::get_config()
  if (!is.null(Region)) {
    config$region <- Region
  }

  if (!startsWith(DBHostname, "https://")) DBHostname <- paste0("https://", DBHostname)
  config$endpoint <- paste(DBHostname, Port, sep = ":")

  svc <- paws.database:::.rds$service(config, op)

  # create new request
  request <- paws.common::new_request(svc, op, input, output)
  request$expire_time <- 900

  # build request
  request <- paws.common:::build(request)
  request$client_info$signing_name <- "rds-db"
  request$http_request$url$raw_query <- sub(
    "&Version=[0-9]{,4}-[0-9]{,2}-[0-9]{,2}", "", request$http_request$url$raw_query
  )
  request$http_request$header$Accept <- NULL

  # sign request
  request <- paws.common:::v4_sign_request_handler(request)
  url <- paws.common:::build_url(request$http_request$url)
  return(substr(url, 9, nchar(url)))
}

I forgot the raw_query is used within the signer so I need to ensure raw_query is formatted correctly before v4 signing.
https://github.com/paws-r/paws/blob/main/paws.common/R/signer_v4.R#L396-L420

@tnederlof
Copy link

tnederlof commented Jan 15, 2025

@DyfanJones interesting! I still have no luck with that token unfortunately. Ill put my code below, I have verified it is using the new method being described in the script.

library(odbc)
library(DBI)
library(paws)


rds_build_auth_token_v2 <- function(DBHostname, Port, DBUsername, Region=NULL) {
  op <- paws.common:::new_operation(
    name = "rds-db", http_method = "GET",
    http_path = "/", host_prefix = "", paginator = list(), stream_api = F
  )
  input <- list(
    Action = "connect",
    DBUser = DBUsername
  )
  output <- list()
  
  config <- paws.common::get_config()
  if (!is.null(Region)) {
    config$region <- Region
  }
  
  if (!startsWith(DBHostname, "https://")) DBHostname <- paste0("https://", DBHostname)
  config$endpoint <- paste(DBHostname, Port, sep = ":")
  
  svc <- paws.database:::.rds$service(config, op)
  
  # create new request
  request <- paws.common::new_request(svc, op, input, output)
  request$expire_time <- 900
  
  # build request
  request <- paws.common:::build(request)
  request$client_info$signing_name <- "rds-db"
  request$http_request$url$raw_query <- sub(
    "&Version=[0-9]{,4}-[0-9]{,2}-[0-9]{,2}", "", request$http_request$url$raw_query
  )
  
  # sign request
  request <- paws.common:::v4_sign_request_handler(request)
  url <- paws.common:::build_url(request$http_request$url)
  return(substr(url, 9, nchar(url)))
}


client = rds()

# add prototype method to client
client$build_auth_token_v2 <- rds_build_auth_token_v2

token <- client$build_auth_token_v2(
  DBHostname='XXXXX.us-east-2.rds.amazonaws.com',
  Port = 5432,
  DBUsername="xxxxx",
  Region="us-east-2"
)


con <- DBI::dbConnect(odbc::odbc(),
                      Driver   = "PostgreSQL",
                      Server   = "xxxxxx.rds.amazonaws.com",
                      Database = "xxxxx",
                      UID      = "xxxxxxx",
                      PWD      = token,
                      Port     = 5432,
)

@DyfanJones
Copy link
Member

Do you have any cloudformation or terraform so I can reproduce the environment? I am abit puzzled why it isn't signing correctly. It should be picking up the correct headers and that 🤔

@tnederlof
Copy link

I appreciate the help, yes it would be good if you could repro, ill try to create a more simple environment with the issue that you can set up.

@DyfanJones
Copy link
Member

I think I know what is going on, within botocore it looks like the host is set to lower case before it is signed. As we leave it alone it causes a miss match in signatures. I will have a better look to where the lower case is set :)

@DyfanJones
Copy link
Member

Found where the url needs to be lower case before signature:
https://github.com/boto/botocore/blob/develop/botocore/auth.py#L75-L91

@tnederlof
Copy link

Good sleuthing! Happy to test another pass at the prototype function.

@DyfanJones
Copy link
Member

DyfanJones commented Jan 16, 2025

This one is going to be a little trickier to test:

remotes::install_github("dyfanjones/paws/paws.common", ref = "cpp_url_parse_fix")

The current paws.common 0.8.0 (dev) won't work as expected with paws 0.7.0. This is down to botocore becoming the new vendor for generating paws as aws-sdk-js is going out of support in 2025.

Ultimately the endpoint regex is getting changed due to the vendor switch. So please make sure you revert your paws.common to cran version after the test (apologises for the inconvenience).

library(odbc)
library(DBI)
library(paws)

client = rds()

# add prototype method to client
client$build_auth_token_v2 <- paws.common:::rds_build_auth_token_v2

token <- client$build_auth_token_v2(
  DBHostname='XXXXX.us-east-2.rds.amazonaws.com',
  Port = 5432,
  DBUsername="xxxxx",
  Region="us-east-2"
)

con <- DBI::dbConnect(odbc::odbc(),
                      Driver   = "PostgreSQL",
                      Server   = "xxxxxx.rds.amazonaws.com",
                      Database = "xxxxx",
                      UID      = "xxxxxxx",
                      PWD      = token,
                      Port     = 5432,
)

NOTE: For other paws use make sure you revert paws.common back to version 0.7.7 (cran version)

@tnederlof
Copy link

tnederlof commented Jan 16, 2025

Hmmm still no luck. Just to make sure I am running the right function here is what I see for paws.common:::rds_build_auth_token_v2

function (DBHostname, Port, DBUsername, Region = NULL) 
{
    op <- new_operation(name = "connect", http_method = "GET", 
        http_path = "/", host_prefix = "", paginator = list(), 
        stream_api = FALSE)
    input <- list(Action = "connect", DBUser = DBUsername)
    output <- list()
    config <- get_config()
    if (!is.null(Region)) {
        config$region <- Region
    }
    if (!startsWith(DBHostname, "https://")) 
        DBHostname <- paste0("https://", DBHostname)
    config$endpoint <- paste(DBHostname, Port, sep = ":")
    metadata <- list(service_name = "rds", endpoints = list(), 
        service_id = "RDS", api_version = "2014-10-31", signing_name = "rds-db", 
        json_version = "", target_prefix = "")
    handlers <- new_handlers("query", "v4")
    svc <- new_service(metadata, handlers, config, op)
    request <- new_request(svc, op, input, output)
    request$expire_time <- 900
    request$http_request$url$raw_query <- build_query_string(request$params)
    request <- v4_sign_request_handler(request)
    url <- build_url(request$http_request$url)
    return(substr(url, 9, nchar(url)))
}

@DyfanJones
Copy link
Member

That is correct, however it would be the change to build_canonical_headers with the host set to lower case that would of been the fix.

If it still isn't working, is it possible for the cloud formation to be shared so I can build the same environment?

@tnederlof
Copy link

Yes, I will work on that, although it will likely be next week for me to pull something together.

@DyfanJones
Copy link
Member

DyfanJones commented Jan 17, 2025

Here is some testing code that will mock the datetime so that paws and boto3 build the token with the same.

edit: prevent raw query being re-ordered:

remotes::install_github("dyfanjones/paws/paws.common", ref = "signature_order")
import boto3
from unittest.mock import patch
import datetime


# Mock datetime
class FixedDatetime(datetime.datetime):
    @classmethod
    def utcnow(cls):
        return cls(2025, 1, 1, 0, 0, 1, tzinfo=datetime.timezone.utc)


def mock_boto_token(DBHostname, Port, DBUsername, Region):
    with patch("datetime.datetime", FixedDatetime):
        token = boto3.client("rds").generate_db_auth_token(
            DBHostname=DBHostname,
            Port=Port,
            DBUsername=DBUsername,
            Region=Region,
        )
    return token
library(paws)
library(reticulate)

# point to boto3 script
reticulate::source_python("rds_mock_token.py")

client = rds()

# add prototype method to client
client$build_auth_token_v2 <- paws.common:::rds_build_auth_token_v2

testthat::local_mocked_bindings(
  Sys.time = function() as.POSIXct("2025/01/01 00:00:01 UTC"),
  .package = "paws.common"
)

host = 'XXXXX.us-east-2.rds.amazonaws.com'
port = 5432L
user = "xxxxx"
region = "us-east-2"

paws_token = client$build_auth_token_v2(
  DBHostname=host,
  Port=port,
  DBUsername=user,
  Region=region
)

boto_token = mock_boto_token(
  DBHostname=host,
  Port=port,
  DBUsername=user,
  Region=region
)

# compare queries built by both
waldo::compare(
  paws_token,
  boto_token
)

My initial results:
#> ✔ No differences

But I have only tested it locally and aws sagemaker.

@tnederlof
Copy link

Very interesting. When I run it I get differences in the Token and X-Amz-Signature. One thing I ran into is I had to add my region to run the boto3 code (boto3.client("rds", region_name=Region).generate_db_auth_token()) because I am in us-east-2 are you testing in us-east-1?

@DyfanJones
Copy link
Member

I don't think region would have a huge impact as region is usually used to build the endpoint. For example I can change the python code to:

import boto3
from unittest.mock import patch
import datetime


# Mock datetime
class FixedDatetime(datetime.datetime):
    @classmethod
    def utcnow(cls):
        return cls(2025, 1, 1, 0, 0, 1, tzinfo=datetime.timezone.utc)


def mock_boto_token(DBHostname, Port, DBUsername, Region):
    with patch("datetime.datetime", FixedDatetime):
        token = boto3.client("rds", region_name = Region).generate_db_auth_token(
            DBHostname=DBHostname,
            Port=Port,
            DBUsername=DBUsername,
        )
    return token

And it should return the same endpoint

library(paws)
library(reticulate)

# point to boto3 script
reticulate::source_python("../inst/python_mocks/rds_generate_db_auth_token.py")

client = rds()

# add prototype method to client
client$build_auth_token_v2 <- paws.common:::rds_build_auth_token_v2

testthat::local_mocked_bindings(
  Sys.time = function() as.POSIXct("2025/01/01 00:00:01 UTC"),
  .package = "paws.common"
)

host = 'XXXXX.us-east-2.rds.amazonaws.com'
port = 5432L
user = "xxxxx"
region = "us-east-2"

paws_token = client$build_auth_token_v2(
  DBHostname=host,
  Port=port,
  DBUsername=user,
  Region=region
)

boto_token = mock_boto_token(
  DBHostname=host,
  Port=port,
  DBUsername=user,
  Region=region
)

# compare queries built by both
waldo::compare(
  paws_token,
  boto_token
)
#> ✔ No differences

There must be something I am missing in the environment 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐞 Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants