0Pricing
R Academy · Lesson

Authentication and API Security

Add API key validation, CORS headers, and rate limiting filters.

Authentication and API Security is a free R Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why API Security Matters

A Plumber API is a public HTTP server. Without authentication, anyone who can reach the port can call your endpoints. Security layers include authentication (who are you?), authorization (what can you do?), input validation, and transport security.

Plumber Filters as Middleware

Filters in Plumber run before the route handler. Use pr_filter(name, function(req, res){...}) to add middleware that inspects every request. Call plumber::forward() to pass to the next filter or route; return early to reject.

# library(plumber)
# pr <- plumb('api.R')
# pr |>
#   pr_filter('logger', function(req, res) {
#     cat(req$REQUEST_METHOD, req$PATH_INFO, '
')
#     plumber::forward()  # must call to continue
#   }) |>
#   pr_run(port = 8000)

API Key Authentication Filter

The most common simple auth pattern for server-to-server APIs is a static API key passed in a header. The filter checks the header on every request and returns 401 if it is missing or wrong.

# In api.R:
# VALID_KEY <- Sys.getenv('API_SECRET_KEY')
#
# #* @filter auth
# function(req, res) {
#   key <- req$HTTP_X_API_KEY
#   if (is.null(key) || key != VALID_KEY) {
#     res$status <- 401L
#     return(list(error = 'Unauthorized'))
#   }
#   plumber::forward()
# }

Checking the Authorization Header

Bearer tokens are passed in the Authorization: Bearer <token> header. Access it via req$HTTP_AUTHORIZATION. Parse with strsplit() to extract the token part, then validate it against your store.

# #* @filter bearer_auth
# function(req, res) {
#   auth_header <- req$HTTP_AUTHORIZATION
#   if (is.null(auth_header) || !startsWith(auth_header, 'Bearer ')) {
#     res$status <- 401L
#     return(list(error = 'Bearer token required'))
#   }
#   token <- substring(auth_header, 8)  # strip 'Bearer '
#   if (!token_is_valid(token)) {
#     res$status <- 401L
#     return(list(error = 'Invalid token'))
#   }
#   plumber::forward()
# }

Skipping Auth with #* @preempt

Some endpoints (health checks, public docs) should skip authentication. Annotate them with #* @preempt auth where auth matches the filter name. Plumber routes the request directly to the handler, bypassing that filter.

# #* Health check — no auth required
# #* @preempt auth
# #* @get /ping
# function() {
#   list(status = 'ok', time = as.character(Sys.time()))
# }
#
# #* Protected endpoint — goes through auth filter
# #* @get /data
# function() {
#   list(secret = 'sensitive data')
# }

CORS with pr_cors()

If your API is called from a browser on a different domain, you must enable CORS (Cross-Origin Resource Sharing). Use pr_cors() to configure allowed origins, methods, and headers without writing raw headers manually.

# library(plumber)
# pr <- plumb('api.R')
# pr |>
#   pr_cors(
#     origin            = 'https://myapp.example.com',
#     methods           = c('GET', 'POST'),
#     headers           = c('Content-Type', 'X-API-Key'),
#     credentials       = TRUE
#   ) |>
#   pr_run(port = 8000)

Input Sanitization — Never Trust User Input

Always validate and sanitize inputs before using them in queries or file operations:

  • Check type: is.numeric(), is.character()
  • Check range: id >= 1 && id <= 1e9
  • Reject unexpected characters: grepl('[^a-zA-Z0-9_]', name)
  • Never interpolate user strings directly into SQL — use parameterized queries
# #* @post /search
# function(req, res) {
#   body <- jsonlite::fromJSON(req$postBody)
#   query <- body$query
#   if (!is.character(query) || nchar(query) > 200) {
#     res$status <- 400L
#     return(list(error = 'query must be a string <= 200 chars'))
#   }
#   if (grepl('[;\'"]', query)) {
#     res$status <- 400L
#     return(list(error = 'Invalid characters in query'))
#   }
#   list(results = search_db(query))
# }

Rate Limiting Concepts

Plumber has no built-in rate limiter, but you can implement one in a filter using a shared environment to track request counts per IP:

  • Record timestamp of each request by IP in an R environment
  • Reject with 429 if count exceeds the limit in the window
  • For production, use a reverse proxy like nginx for rate limiting
# request_log <- new.env()
#
# #* @filter rate_limit
# function(req, res) {
#   ip <- req$REMOTE_ADDR
#   now <- as.numeric(Sys.time())
#   if (!exists(ip, envir = request_log)) assign(ip, c(), envir = request_log)
#   times <- get(ip, envir = request_log)
#   times <- times[times > now - 60]   # last 60 seconds
#   if (length(times) >= 60) { res$status <- 429L; return(list(error='Too Many Requests')) }
#   assign(ip, c(times, now), envir = request_log)
#   plumber::forward()
# }

Storing API Keys Securely

Never hardcode secrets in source files. Store them in environment variables and read them at startup with Sys.getenv(). Use a .env file locally (excluded from git) and inject secrets via the deployment environment in production.

# In .env (never commit this file):
# API_SECRET_KEY=my_super_secret_key_here
#
# In api.R:
# VALID_KEY <- Sys.getenv('API_SECRET_KEY', unset = '')
# if (nchar(VALID_KEY) == 0) stop('API_SECRET_KEY not set')
#
# Load .env locally (devtools::load_dot_env or Sys.setenv):
# readRenviron('.env')
cat('Sys.getenv reads API keys without exposing them in source
')

Attaching User Context to the Request

After validating a token in the auth filter, attach decoded user info to the req object so downstream handlers can access it without re-validating. Custom fields on req persist through the filter chain.

# #* @filter auth
# function(req, res) {
#   token <- req$HTTP_AUTHORIZATION
#   user <- validate_token(token)  # returns list(id=1, role='admin')
#   if (is.null(user)) { res$status <- 401L; return(list(error='Unauthorized')) }
#   req$user <- user   # attach to request
#   plumber::forward()
# }
#
# #* @get /profile
# function(req) {
#   list(user_id = req$user$id, role = req$user$role)
# }

Error Handling with tryCatch

Wrap your endpoint logic in tryCatch() to catch unexpected errors and return a clean 500 response instead of crashing the worker or leaking a stack trace to the caller.

# #* @get /risky/<id:int>
# function(id, res) {
#   tryCatch({
#     result <- risky_db_call(id)
#     list(data = result)
#   }, error = function(e) {
#     message('Error in /risky: ', conditionMessage(e))
#     res$status <- 500L
#     list(error = 'Internal server error')
#   })
# }

Quick Check: @preempt Annotation

What does the #* @preempt auth annotation do to a Plumber endpoint?

API Security Recap

Securing a Plumber API involves layered defenses:

  • pr_filter('auth', ...) — inspect every request in middleware
  • req$HTTP_AUTHORIZATION / req$HTTP_X_API_KEY — read auth headers
  • #* @preempt auth — skip auth for public endpoints
  • pr_cors() — configure browser cross-origin access
  • Input validation before any DB or file operation
  • Sys.getenv() for secrets — never hardcode keys

Frequently asked questions

Is the “Authentication and API Security” lesson free?

Yes — the full text of “Authentication and API Security” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.

What will I learn in “Authentication and API Security”?

Add API key validation, CORS headers, and rate limiting filters. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start R Academy?

No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Authentication and API Security” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this R Academy lesson?

Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Introduction to Plumber and REST
  2. Creating GET and POST Endpoints
  3. Authentication and API Security
  4. Deploying Plumber APIs to Production
← Back to R Academy