Skip to content

How To Access Federated Api Using Saml2 And Adfs

##########################################################################
# modified from: https://aws.amazon.com/blogs/security/how-to-implement-federated-api-and-cli-access-using-saml-2-0-and-ad-fs/
##########################################################################

##########################################################################
# Packages required
##########################################################################
library(httr)
library(xml2)
# getPass is used within the function when password isn't provided
# base64enc is used to decode samlassertion
##########################################################################

# wrapper to access
adfs_saml2 <- function(adfs_url, region, username = NULL, password = NULL) {
  # Get the federated credentials from the user
  if (is.null(username)) {
    username <- readline("Username:")
  }
  if (is.null(password)) {
    password <- getPass::getPass()
  }

  set_config(config(ssl_verifypeer = 0L))
  # get first page
  formresponse <- GET(adfs_url)

  on.exit(handle_reset(adfs_url))

  idpauthformsubmiturl <- formresponse$url

  formsoup <- content(formresponse)

  payload <- list()
  for (inputtag in xml_find_all(formsoup, ".//INPUT|.//input")) {
    name <- if (is.na(xml_attr(inputtag, "name"))) {
      ""
    } else {
      xml_attr(inputtag, "name")
    }
    value <- if (is.na(xml_attr(inputtag, "value"))) {
      ""
    } else {
      xml_attr(inputtag, "value")
    }
    if (grepl("user", tolower(name))) {
      payload[[name]] <- username
    } else if (grepl("email", tolower(name))) {
      payload[[name]] <- username
    } else if (grepl("pass", tolower(name))) {
      payload[[name]] <- password
    } else {
      payload[[name]] <- value
    }
  }

  for (inputtag in xml_find_all(formsoup, ('.//FORM|.//form'))) {
    action <- xml_attr(inputtag, 'action')
    loginid <- xml_attr(inputtag, 'id')
    if (!is.na(action) && loginid == "loginForm") {
      parsedurl <- parse_url(adfs_url)
      idpauthformsubmiturl <- paste0(parsedurl$scheme, "://", parsedurl$hostname, action)
    }
  }

  Cookies <- unlist(as.list(formresponse$cookies))

  # get samlassertion
  response <- POST(
    idpauthformsubmiturl,
    body = payload,
    set_cookies(.cookies = Cookies),
    encode = "form"
  )

  soup <- content(response)
  SAMLResponse <- NA
  for (inputtag in xml_find_all(soup, ('.//input'))) {
    name <- if (!is.na(xml_attr(inputtag, 'name'))) {
      xml_attr(inputtag, 'name')
    } else {
      ""
    }
    if (name == 'SAMLResponse') {
      SAMLResponse <- xml_attr(inputtag, 'value')
    }
  }

  if (is.na(SAMLResponse)) {
    stop('Response did not contain a valid SAML assertion')
  }

  # decode saml assertion so that can get roles for user to choose from
  root <- readBin(base64enc::base64decode(SAMLResponse), "character")
  saml2attribute <- read_xml(root)

  # get correct level to iterate over
  child3 <- xml_children(xml_children(xml_children(saml2attribute)))

  # get aws roles
  awsroles <- list()
  for (saml in child3) {
    if (isTRUE(xml_attr(saml, "Name") == "https://aws.amazon.com/SAML/Attributes/Role")) {
      awsroles <- c(awsroles, xml_text(xml_children(saml)))
    }
  }

  # safe way to present roles to user (hides saml roles)
  aws_saml_role <- lapply(seq_along(awsroles), function(i) {
    aws_role_split <- unlist(strsplit(awsroles[[i]], ","))
    saml <- grepl("saml-provider", aws_role_split)
    names(aws_role_split) <- c("role_arn", "principal_arn")[as.numeric(saml) + 1]
    aws_role_split
  })

  # asks user which role to choose
  writeLines("Please choose the role you would like to assume:")
  for (i in seq_along(aws_saml_role)) {
    writeLines(sprintf("[%i]: %s", i, aws_saml_role[[i]]["role_arn"]))
  }
  choosen_role <- as.numeric(readline("Selection: "))
  aws_saml_role <- aws_saml_role[[choosen_role]]

  list(SamlRole = aws_saml_role, SAMLResponse = SAMLResponse)
}

##########################################################################
# Variables

# region: The default AWS region that this script will connect
# to for all API calls
region <- 'us-west-2'


# idpentryurl: The initial URL that starts the authentication process.
idpentryurl <- 'https://<fqdn>/adfs/ls/IdpInitiatedSignOn.aspx?loginToRp=urn:amazon:webservices'

##########################################################################

##########################################################################
# connect to adfs and get saml2 assertion
obj <- adfs_saml2(idpentryurl, region)


cred <- paws::sts()$assume_role_with_saml(
  RoleArn = obj$SamlRole["role_arn"],
  PrincipalArn = obj$SamlRole["principal_arn"],
  SAMLAssertion = obj$SAMLResponse
)

##########################################################################

# Use the AWS STS token to list all of the S3 buckets
config <- list(
  credentials = list(
    creds = list(
      access_key_id = cred$Credentials$AccessKeyId,
      secret_access_key = cred$Credentials$SecretAccessKey,
      session_token = cred$Credentials$SessionToken
    )
  )
)

# connect to AWS S3 using temporary SAML credentials
S3 <- paws::s3(config)

# list all s3 buckets
sapply(S3$list_buckets()$Buckets, function(x) x$Name)