"JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties." (jwt.io)
The JWT stratgey allows to use JSON Web Tokens (JWT) to secure your plumber endpoints. A great introduction to JWT can be found here. JWT can be used to secure REST APIs without sessions. They are considered to be stateless although they can also be used as stateful session tokens.
JSON Web Tokens (JWT) can contain claims. "Claims are statements about an entity (typically, the user) and additional data" (https://jwt.io/introduction/). They are expressed as key-value pairs.
There are three different types of claims: registered, public and private claims. All types of claims are implemented in the same manner, they only differ in whether and where the claims are registered with the Internet Assigned Numbers Authority (IANA). For example, the iss
claim is a registered claim defined in the JWT standard RFC 7519 and registered at IANA. See the JWT Introduction of jwt.io for more details.
sealr
allows you to check for the validity of all types of claims in a given JWT using the claims
argument of the sealr::is_authed_jwt
function.
In the code below, you find a small example of how to implement a JWT strategy in an application.
The application consists of three routes. The first route allows your users to login and issues a JWT. The second route is an open route that does not require authentication. The third route requires authentication using the JWT.
The JWT filter looks like this:
# integrate the jwt strategy in a filter pr$filter("sealr-jwt", function (req, res) { # simply call the strategy and forward the request and response sealr::authenticate(req = req, res = res, is_authed_fun = sealr::is_authed_jwt, token_location = "header", secret = secret) })
:warning: Please change the secret to a super secure secret of your choice. Please notice that you have to preempt = c("sealr-jwt")
to routes that should not be protected.
Copy the code from below in a new R file and
save it under jwt_simple_example.R
. In the R console, run:
plumber::plumb("jwt_simple_example.R")
This will make the API available at localhost:9090
.
In order to run this example, you need the following packages installed:
Using curl, Postman or a similar tool for sending HTTP requests, send a POST request with the details of one of the two users that are in the API's "database" (in this simplified example, a data frame).
users <- data.frame(id = integer(), name = character(), password = character(), stringsAsFactors = FALSE) # create test user users <- rbind(users, data.frame(id = 1, user = "jane@example.com", password = bcrypt::hashpw("12345"), stringsAsFactors = FALSE)) users <- rbind(users, data.frame(id = 2, user = "bob@example.com", password = bcrypt::hashpw("45678"), stringsAsFactors = FALSE)) kableExtra::kable(users, format = "markdown")
For example, in curl:
curl --data '{"user": "jane@example.com", "password": "12345"}' localhost:9090/authentication
gives back the JWT:
["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NTE0NDk5NTcsInVzZXJJRCI6MX0.0563N-dcz9zY-NF9DQpUnHIONZRWmZU1rb894xxHcNU"]
Note: You might get different JWTs as jose::jwt_encode_hmac
automatically adds the time when the JWT was issued as a claim (iat
claim). However, those examples should still work because we do not add an expiration time to the token - something you should definetely consider for production use cases.
Trying to authenticate with a user that is not in the database fails:
curl --data '{"user": "drake@example.com", "password": "10111213"}' localhost:9090/authentication
{"status":["Failed."],"code":[401],"message":["User or password wrong."]}
Everyone can access the /
route because it does not require authentication - the sealr-jwt
filter is preempt
ed for this route:
curl localhost:9090 ["Access to route without authentication was successful."]
Trying to access the /secret
route without a JWT fails because it goes through the sealr-jwt
filter where the sealr::jwt
function will check for the correct authentication details - in this case a valid JWT in the Authorization header.
curl localhost:9090/secret
{"status":["Failed."],"code":[401],"message":["Authentication required."]}
Use the JWT obtained with the first curl command to make an authenticated request to this route.
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NTE0NDk5NTcsInVzZXJJRCI6MX0.0563N-dcz9zY-NF9DQpUnHIONZRWmZU1rb894xxHcNU" localhost:9090/secret ["Access to route requiring authentication was successful."]
In this example implementation (see full code below), we have two filters:
The filter sealr-jwt
simply checks whether the user is authenticated and that the issuer claim iss
is set to mygreatplumberapi
, the value we set in the authentication
route.
The second filter sealr-jwt-admin-only
additionally checks whether the user is an admin by validating that the claim admin
is TRUE
.
# integrate the jwt strategy in a filter pr$filter("sealr-jwt", function (req, res) { # simply call the strategy and forward the request and response sealr::authenticate(req = req, res = res, is_authed_fun = is_authed_jwt, secret = secret, claims = list(iss = "mygreatplumberapi")) }) pr$filter("sealr-jwt-admin-only", function (req, res) { # simply call the strategy and forward the request and response sealr::authenticate(req = req, res = res, is_authed_fun = is_authed_jwt, secret = secret, claims = list(iss = "mygreatplumberapi", admin = TRUE)) })
We now have two levels of access using filters: routes that are open to all authenticated users with a JWT issued by mygreatplumberapi
and routes that are accessible to admins only. The former requires preempt
ing the more restrictive sealr-jwt-admin-only
filter.
Unfortunately, it is currently not possible to extend this filter-based authorization mechanism to more than two authorization "levels" because plumber
does not allow for preempting more than one filter per route.
This problem is on the radar of the plumber
team and they'll provide the opportunity to impose filters on specific
endpoints in the future (kind of "reverting" the preempt
logic). See this plumber issue.
As a workaround, you could put your authentication / authorization checks in the individual endpoints.
In this case, use is_authed_*
functions instead of the authenticate
wrapper.
Copy the code from below in a new R file and
save it under jwt_claims_example.R
. In the R console, run:
plumber::plumb("jwt_claims_example.R")
This will make the API available at localhost:9090
.
In order to run this example, you need the following packages installed:
Using curl, Postman or a similar tool for sending HTTP requests, send a POST request with the details of one of the two users that are in the API's "database" (in this simplified example, a data frame).
# define a user database # you should probably use a SQL database instead of data frames users <- data.frame(id = integer(), name = character(), password = character(), admin = logical(), gender = character(), stringsAsFactors = FALSE) # create test user users <- rbind(users, data.frame(id = 1, user = "jane@example.com", password = bcrypt::hashpw("12345"), admin = TRUE, gender = "woman", stringsAsFactors = FALSE)) users <- rbind(users, data.frame(id = 2, user = "bob@example.com", password = bcrypt::hashpw("45678"), admin = FALSE, gender = "man", stringsAsFactors = FALSE)) kableExtra::kable(users, format = "markdown")
Get the JWT for both users:
curl --data '{"user": "jane@example.com", "password": "12345"}' localhost:9090/authentication ["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NjE0NiwiYWRtaW4iOnRydWUsImdlbmRlciI6IndvbWFuIiwidXNlcklEIjoxfQ.AZqJFuXZkjwKbnULHfJVmBapFhZpBgLIUuX7HOJAUhU"]
curl --data '{"user": "bob@example.com", "password": "45678"}' localhost:9090/authentication ["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NTgzNCwiYWRtaW4iOmZhbHNlLCJnZW5kZXIiOiJtYW4iLCJ1c2VySUQiOjJ9.WjRD5aIaqgApWJ-bf0VosbMZ3ovDyvRVvYug-5egL8s"]
Note: You might get different JWTs as we also add the time when the token was issued as a claim (iat
). However, those examples should still work because we do not add an expiration time to the token - something you should definetely consider for production use cases.
Both users can access the /secret
route because they both have valid JWT issued by mygreatplumberapi
. The route preempt
s the more restrictive sealr-jwt-admin-only
filter so even non-admin Bob has access.
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NTc3NSwiYWRtaW4iOnRydWUsImdlbmRlciI6IndvbWFuIiwidXNlcklEIjoxfQ.FXLTGUcsn8yuiS7VqoGEjw94zQsmO6sYdWJeLeS-PhE" localhost:9090/secret ["Access to route that requires authentication was successful."]
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NTgzNCwiYWRtaW4iOmZhbHNlLCJnZW5kZXIiOiJtYW4iLCJ1c2VySUQiOjJ9.WjRD5aIaqgApWJ-bf0VosbMZ3ovDyvRVvYug-5egL8s" localhost:9090/secret ["Access to route that requires authentication was successful."]
In contrast, only Jane can access the /secret-admin-only
route.
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NTc3NSwiYWRtaW4iOnRydWUsImdlbmRlciI6IndvbWFuIiwidXNlcklEIjoxfQ.FXLTGUcsn8yuiS7VqoGEjw94zQsmO6sYdWJeLeS-PhE" localhost:9090/secret-admin-only ["Access to route that requires admin authorization was successful."]
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJteWdyZWF0cGx1bWJlcmFwaSIsImlhdCI6MTU1MzY3NTgzNCwiYWRtaW4iOmZhbHNlLCJnZW5kZXIiOiJtYW4iLCJ1c2VySUQiOjJ9.WjRD5aIaqgApWJ-bf0VosbMZ3ovDyvRVvYug-5egL8s" localhost:9090/secret-admin-only {"status":["Failed."],"code":[401],"message":["Authentication required."]}
The Google OAuth2 strategy allows you to use Google’s OpenID Connect interface to authenticate and authorize your users. A detailed introduction and best practices can be found here. The interface uses JWTs. Hence, the process can be considered stateless. Addtionally, the user tokens can be used to access Google APIs.
In order to run this example, you need to obtain OAuth2.0 credentials for your plumber API so that Google later knows that it is authenticating the user to a legitimate application. For this, create a new project in the Google API Console - you may need to authorize your Google account first if you are not yet a user of Google's developer platform.
Once you have created your project, follow the instructions on "Obtain OAuth 2.0 credentials" here. When you have to select the application type, select "Other". Store the client ID and the client secret as environment variables in your R session using the following commands.
Sys.setenv("GOOGLE_CLIENT_ID" = "yourid") Sys.setenv("GOOGLE_CLIENT_SECRET" = "yoursecret")
This will make the client and secret available for your current R session. If you want
to make them available beyond your current session, use usethis::edit_r_environ
and
add them in the file that opens like this:
GOOGLE_CLIENT_ID="yourid" GOOGLE_CLIENT_SECRET="yoursecret"
Save and close the file.
Copy the code from below in a new R file and
save it under oauth2_google_simple_example.R
. In the R console, run:
plumber::plumb("oauth2_google_simple_example.R")
This will make the API available at localhost:9090
.
In order to run this example, you need the following packages installed:
Open your browser and enter http://localhost:9090/authentication/
in the address bar.
You'll be redirected to Google. Authorize your application / plumber API.
You'll be again redirected to a JSON response that, depending on your browser, should look something like this (tokens are blacked out):
It contains:
access_token
: the token you would need if you wanted to access any of Google's APIs in your plumber API. If you only use Google to authenticate users, this will not be necessary.expires_in
: how long the access token is valid in seconds. This value is set by Google. In this case, the access token is valid for one hour. refresh_token
: the token you can could to refresh your access token. We have not implemented the refresh logic in this example though. scope
: the scope your plumber API requested to authorize from the user. In this example, we only requested the "userinfo.profile" scope. token_type
: type of token. This will always be "Bearer". Prepend this to your HTTP Authorization Header (see below). id_token
: The ID token. A JSON Web Token (see section on JWT) that contains information about the identify of the user. This token is signed by Google. This is the token you send in the HTTP Authorization header (see below).See also the explanation of the return values on Google's OpenID Connect website.
Open a terminal and enter the following command, replacing the YOUR_ID_TOKEN with
the id_token
from your response.
curl -H "Authorization: Bearer YOUR_ID_TOKEN" localhost:9090/secret
The ID token will be quite long, so maybe first edit this command in your text editor of choice before copying it to the terminal. Hit enter.
You should get back:
{"message":["Successfully accessed the secret endpoint."]}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.