Implementing OpenID Connect (OIDC) in R
I am working on a rust project that I want to use OpenID Connect for. I’m struggling to wrap my head around it, so naturally, I implemented it in R to understand it better.
What is OIDC?
OpenID Connect (OIDC) is an authentication standard based on OAuth 2.0. The hope is that most identity providers (IDP) can have an implementation of OIDC so that plugging in their authentication system is pretty straight forward.
OIDC discovery
Each OIDC provider has an
{issuer_url}/.well-known/openid-configuration URL which contains
information about the authentication provider. This is a public facing
document that can be used to find endpoints and other information
For this example, I’ve created a free account at
Auth0 and made an application. I’ll store the url
in a variable called issuer_url
issuer_url <- "https://dev-2ts7ytkts28hfj4o.us.auth0.com"
Accessing the openid-configuration is a simple get request. We’ll
create an oidc_discovery() function. This will return a list and we
will give it a class oidc_provider
{
res <- |>
|>
|>
}
Tip
I’ve also given this object a nicer print method based on the
httr2_oauth_clientclass in{httr2}.{ # adapted from httr2:::print.httr2_request cli:: lines <- cli:: }
Using this gives us a very informative list that we will use for identifying our authorization endpoints.
provider <-
<oidc_provider>
issuer: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/'
authorization_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/authorize'
token_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/oauth/token'
device_authorization_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/oauth/device/code'
userinfo_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/userinfo'
mfa_challenge_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/mfa/challenge'
jwks_uri: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/.well-known/jwks.json'
registration_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/oidc/register'
revocation_endpoint: 'https://dev-2ts7ytkts28hfj4o.us.auth0.com/oauth/revoke'
scopes_supported: list
response_types_supported: list
code_challenge_methods_supported: list
response_modes_supported: list
subject_types_supported: list
token_endpoint_auth_methods_supported: list
claims_supported: list
request_uri_parameter_supported: FALSE
request_parameter_supported: FALSE
id_token_signing_alg_values_supported: list
token_endpoint_auth_signing_alg_values_supported: list
end_session_endpoint:
'https://dev-2ts7ytkts28hfj4o.us.auth0.com/oidc/logout'
The information in this object will be used for our oauth flows with
httr2.
OIDC Client Object
In httr2, we create an httr2_oauth_client object to be used for our
authentication flows. We will generalize that approac and create
oidc_client().
In this function, we will store the redirect_uri into the client
itself as well as tack on the oidc_client subclass. This will give us
a nicer print method and prevent us from having to put in the
redirect_uri multiple times.
{
client <-
client <- redirect_uri
<-
client
}
This function fetches the client id and secret from environment variables. This is because we do not want to store these variables directly in our code.
Use usethis::edit_r_environ() to set these variables globally.
Alternatively, you can use something like config to have a
config.yml file or an alternative environment management system. But
at the end of the day just please do not store your credentials in your
code!!!!
For Auth0, you have to specify which redirect URIs can be trusted. In my
case I set it to http://localhost:3000/oauth/callback in my
application settings.
client <-
<oidc_client/httr2_oauth_client>
name: bf83aacb811320e5da430601736f1286
id:
secret: <REDACTED>
token_url: https://dev-2ts7ytkts28hfj4o.us.auth0.com/oauth/token
auth: oauth_client_req_auth_body
redirect_uri: http://localhost:3000/oauth/callback
This client will now be used for our authentication steps.
OAuth2 Code Flow
The most secure method of authentication with OAuth2 is the code flow.
This is also the most common when building web applications. It will
send you to the external provider to authenticate there, then return you
to the app when complete with an access_token and an id_token.
Here we create the oidc_flow_auth_code() function. The authorization
endpoint will likely be different for providers. This is why we fetch it
from the provider itself.
{
}
Now that I’m looking at this again, it may be worth storing the the authorization endpoint into the client too…
When we authenticate with OIDC we most also provide the openid scope.
This indicates to the provider that the OIDC protocol will be used.
Additionally, OIDC uses something called json web-tokens (JWT).
JWTs have “claims” associated with them. This is basic informations
about the user that is authenticated. These get stored alongside the
access_token as an id_token.
The standard
claim
profile will give you a lot of basic information about an end-user. It
wraps up the name, family_name, given_name, middle_name, nickname,
preferred_username, profile, picture, website, gender, birthdate,
zoneinfo, and locale claims.
Specify the claim you want after openid in the scope argument
token <-
<httr2_token>
token_type: Bearer
access_token: <REDACTED>
expires_at: 2024-11-29 11:07:12
id_token: <REDACTED>
scope: openid profile
With this you’ve now authenticated using OIDC. Though you may want to
access the user information in the token. We can do that by decoding the
id_token.
Accessing Claims
Here we create a function parse_id_token() which takes the contents of
token$id_token and parses it into something human representable.
token$id_token
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImV0WWVUTjhseGJ6VENZblBMNnhxSyJ9.eyJnaXZlbl9uYW1lIjoiSm9zaWFoIiwiZmFtaWx5X25hbWUiOiJQYXJyeSIsIm5pY2tuYW1lIjoiam9zaWFoLnBhcnJ5IiwibmFtZSI6Ikpvc2lhaCBQYXJyeSIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5...truncate"
This is base64 encoded nonsense. Below is an opinionated way to decode
this. I utilize the {b64} package
for fast decoding. Then use
{yyjsonr}
for fast json parsing.
{
parts <-
b64:: |>
|>
rlang:: |>
}
$header
$header$alg
[1] "RS256"
$header$typ
[1] "JWT"
$header$kid
[1] "redacted"
$payload
$payload$given_name
[1] "Josiah"
$payload$family_name
[1] "Parry"
$payload$nickname
[1] "josiah.parry"
$payload$name
[1] "Josiah Parry"
$payload$picture
[1] "redacted"
$payload$updated_at
[1] "2024-11-28T00:46:24.934Z"
$payload$iss
[1] "https://dev-2ts7ytkts28hfj4o.us.auth0.com/"
$payload$aud
[1] "mi3FRXJuJarrM7rFBDr0N270l84ANSXo"
$payload$iat
[1] 1732820832
$payload$exp
[1] 1732856832
$payload$sub
[1] "redacted"
$payload$sid
[1] "redacted"
Authenticating requests with OIDC
However, you may want to wrap your requests with your OIDC auth provider.
{
req |>
}
Accessing UserInfo
Each OIDC provider also has a UserInfo endpoint that can be accessed
for user-level claims.
We can wrap this up as well:
{
|>
|>
|>
}
Note that this will only give you the user information that is associated with the claims used to authenticate with as well.