Tell APIs who you are

TODO

  • Emphasize difference between working with what people have done vs understanding how things SHOULD work (knowing the OAuth2 specification backwards and forwards isn’t helpful when people use nonstandard terminology or implement something weird; PKCE often doesn’t work, for example)
  • Make sure this makes sense before pagination.

️✅ Learning objectives

  • Provide information in your request about how you’re accessing the API.
  • Find authentication information in API docs.
  • Authenticate a request with an API key.
  • Authenticate a request with OAuth.
library(httr2)

How can I tell the API how I’m accessing it?

What is a user agent?

  • “User agent” = software that accesses the web
  • User-Agent request header = string IDing the “user agent”
    • Request header = extra info sent with request
  • {httr2} attaches a default User-Agent header
httr2::request("https://example.com") |> httr2::req_dry_run()
#> GET / HTTP/1.1
#> Host: example.com
#> User-Agent: httr2/1.0.1 r-curl/5.2.1 libcurl/8.3.0
#> Accept: */*
#> Accept-Encoding: deflate, gzip

Should I override the default user agent?

  • Are you hitting the API a lot?
  • Are you providing reusable code for others to hit the API?
  • Does the API documentation mention it?

How do I override the default user agent?

httr2::req_user_agent(req, string = NULL)

httr2::request("https://example.com") |> 
  httr2::req_user_agent("MyUserAgent/1.0 (more details; separate with ;)") |> 
  httr2::req_dry_run()
#> GET / HTTP/1.1
#> Host: example.com
#> User-Agent: MyUserAgent/1.0 (more details; separate with ;)
#> Accept: */*
#> Accept-Encoding: deflate, gzip

An experimental .Rprofile user agent function

.httr2_ua <- function() {
  # Recreate the default httr2 string.
  versions <- c(
    httr2 = as.character(utils::packageVersion("httr2")),
    `r-curl` = as.character(utils::packageVersion("curl")),
    libcurl = curl::curl_version()$version
  )
  paste0(names(versions), "/", versions, collapse = " ")
}
.req_ua <- function(req) {
  httr2_string <- .httr2_ua()
  me <- "Jon Harmon"
  url <- "https://wapir.io"
  email <- "jonthegeek+useragent@gmail.com"
  string <- glue::glue(
    "{httr2_string} ({me}; {url}; mailto:{email})"
  )
  httr2::req_user_agent(req, string = string)
}

How can I find authentication information?

What is authentication?

  • Authentication: Verifying who you are.
  • Authorization: Granting permissions to do things (based on authentication)
  • Auth: Used interchangeably for both

What are some types of authentication?

🔴 HTTP Basic: username + password sent with request

        📜 deed to your house

🟠 API Key: password-like thing sent with request

        🔑 key to your house

🟡 Bearer Token: shorter-lived, limited key

        💳 keycard

🟢 OAuth: multistep process to generate a bearer token

        🕵️ background check to issue keycard

API Auth Documentation

OpenAPI: securitySchemes

If you have the APID, use it!

How can I prepare my system for authentication?

Practice safe git

  • Run usethis::git_vaccinate()
  • Often usethis::use_git_ignore(".Renviron")

Use keyring

  • install.packages("keyring")
  • keyring::key_set(service)
    • keyring::key_set("FEC_API_KEY")
  • keyring::key_set_with_value(service, password = NULL)
  • keyring::key_get(service)
  • May need to copy keyring to env for packages
    • Sys.setenv(FEC_API_KEY = keyring::key_get("FEC_API_KEY"))

How can I authenticate a request using API keys?

Where do I send API keys?

  • in: query
    • httr2::req_url_query(.req, ...)
  • in: header
    • httr2::req_headers(.req, ..., .redact = NULL)
    • .redact = character vector of headers to hide in print
  • in: cookie
    • httr2::req_headers(.req, Cookie = "name=val1; name2=val2", .redact = "Cookie")

How can I authenticate FECAPI requests?

OpenFEC APID

request("https://api.open.fec.gov/v1") |> 
  req_headers("X-Api-Key" = "DEMO_KEY", .redact = "X-Api-Key")
#> <httr2_request>
#> GET https://api.open.fec.gov/v1
#> Headers:
#> • X-Api-Key: '<REDACTED>'
#> Body: empty
request("https://api.open.fec.gov/v1") |> 
  req_headers("X-Api-Key" = keyring::key_get("FEC_API_KEY"), .redact = "X-Api-Key")
#> <httr2_request>
#> GET https://api.open.fec.gov/v1
#> Headers:
#> • X-Api-Key: '<REDACTED>'
#> Body: empty

Authenticating with nectar

{nectar} 📦 translates APID to {httr2}

request("https://api.open.fec.gov/v1") |> 
  nectar::req_auth_api_key(
    location = "header", 
    parameter_name = "X-Api-Key", 
    api_key = Sys.getenv("FEC_API_KEY")
  )
#> <httr2_request>
#> GET https://api.open.fec.gov/v1
#> Headers:
#> • X-Api-Key: '<REDACTED>'
#> Body: empty

How can I authenticate a request using OAuth?

Oauth terminology: Participants

term(s) meaning
user the person who you’re acting as
application, app your R code
client, oauth application you create this at oauth host
oauth host the API

Oauth terminology: Things

term(s) meaning
scope string(s) describing specific capabilities
authorization code very temporary key
oauth token, token the real key, often with extra info

Oauth terminology: Places

term(s) meaning
authorization url where to send initial request
redirect url(s) where host can send auth codes
token url where app can exchange auth codes

The OAuth “dance”

  • user to app: Hit this API for me!
  • <app sends user to host @ auth url in browser>
  • host to user: Can client act as you with these scopes?
  • user to host: Yes
  • <host sends user to app @ redirect url with auth code>
  • app directly to host @ token url: Here’s my client + user’s auth code
  • host directly to app: Here’s an oauth token for that user (with the requested permissions)

OAuth credential dangers

  • 🟢 Client id: Like knowing package name
  • 🟡 Authorization code: Unlikely to be an issue
    • Only sent to provided redirect url
    • Extremely short lived (often minutes or less)
  • 🟡 Refresh token:
    • This + client secret for access token
    • Usually revoked if you auth from scratch

OAuth credential dangers (cont)

  • 🟡 Client secret: Iffy
    • Can pretend to be you (user still needs to say ok)
    • Can your client do anything special?
      • Installed (e.g. Slack app)?
      • API usage limits (e.g. YouTube)?
  • 🟠 Access token: The thing we’re protecting
    • Can do whatever it’s authorized to do
    • Usually easy to revoke
  • 🔴 Username + password: We don’t want to know these

How can I use OAuth in R?

Configure your client

Construct one client object for your code

httr2::oauth_client(
  id, 
  token_url, 
  secret = NULL, 
  key = NULL, 
  auth = c("body", "header", "jwt_sig"), 
  auth_params = list(), 
  name = hash(id)
)

OAuth client demo

yt_client <- oauth_client(
  id = Sys.getenv("YOUTUBE_CLIENT_ID"), 
  token_url = "https://oauth2.googleapis.com/token",
  secret = Sys.getenv("YOUTUBE_CLIENT_SECRET")
)

httr2::req_oauth_auth_code()

httr2::req_oauth_auth_code(
  req,
  client,
  auth_url,
  scope = NULL,
  pkce = TRUE,
  auth_params = list(),
  token_params = list(),
  redirect_uri = oauth_redirect_uri(),
  cache_disk = FALSE,
  cache_key = NULL
)

Oauth request demo

playlists <- request("https://youtube.googleapis.com/youtube/v3") |> 
  req_url_path_append("playlists") |> 
  req_url_query(part = "snippet", mine = TRUE, maxResults = 50) |> 
  req_oauth_auth_code(
    client = yt_client, 
    auth_url = "https://accounts.google.com/o/oauth2/v2/auth",
    scope = "https://www.googleapis.com/auth/youtube",
    redirect_uri = "http://127.0.0.1:8888"
  ) |> 
  req_perform()

Leftovers

Automating OAuth

  • If you can, use httr2 cache: easiest, but
    • auto-deletes when 30 days old
    • fills logs w/ “Caching httr2 token in …” messages
  • httr2::req_oauth_bearer_jwt() if you have JSON web token (service account)
  • httr2::req_oauth_refresh() if you have a refresh token
    • httr2::oauth_flow_auth_code() once to get refresh

Browser cookies

This will feel hacky because it is hacky.

  • Install EditThisCookie browser extension
  • Use API in browser
  • Open EditThisCookie extension
  • Options > “Choose the preferred export format for cookies” > Netscape HTTP Cookie File
  • Open EditThisCookie extension
  • Export
  • Paste into a file at path
  • httr2::req_cookie_preserve(req, path)

Meeting Videos

Cohort 1

Meeting chat log
LOG