In this post, we'll be learning how to implement OAuth 2.0 in Rust by writing a backend service that will interact with Google OAuth and will interact with OpenID Connect ("OIDC") service from Google to retrieve a user's email. We'll first learn to use the oauth2 library to authorise our users using database-backed sessions to keep them authenticated with a private cookie jar, then we'll use a middleware for Rust authentication to authenticate users and insert an extension to the request from the middleware.
The final code for the repository can be found here.
Set Up
Before we get started, you'll want the following:
- A project in Google Cloud Console (you can get started here - it's free!)
We'll want to also install sqlx-cli
, which we can do by running the following command:
Once we've created our project, we'll want to use sqlx migrate add schema
to create our initial schema file which you will be able to find in the migrations folder. Once you open the file, it'll be empty with a simple comment to add your migrations - in which we'll add the following:
When we run our app we'll run the migrate macro, which will automatically attempt to run our migrations and add a new migration entry to the table so it won't automatically try to run the migration again.
Getting Started
To get started, you'll want to create a new project by running the following:
We'll want to pick "axum" as the framework. For the purposes of the project we will refer to the project name as "oauth-rust".
Looking to deploy? Make sure you enable initialising your project on the Shuttle servers!
Next we'll want to install our dependencies - copy the script below to install everything in one go:
Next you'll want to create a Secrets.toml
file in the root of your backend that holds all of our secret variables - you'll
want to make sure you have at least the following, in the following format:
Then we'll want to get started on setting up our main entrypoint function! We can get it set up like so:
Before we go any further, we should set up our error handling type so that we can propagate errors up the call stack instead of trying to either unwrap everything or manually handle every single error.
Here, note that the #[from]
attribute allows us to directly implement From<T>
for our enum. The #[error("...")]
attribute allows us to write an error message while still including the original error.
To make our error type compatible with Axum, we need to implement the IntoResponse
trait. We can do this like so:
But how do I use OAuth?
First, we will need to write a function to create an oauth2::BasicClient
. This client can take any OAuth authorization endpoint URL and token endpoint URL (as long as they're both from the same OAuth service). We can pass in our Google OAuth secrets that we created earlier, as you'll be able to see below. Our redirect URL should be an endpoint that we create on our side so when the user gets successfully authorised, they get sent back to our application with a code we can exchange for a token that allows a user to stay authenticated.
Now that we've created our BasicClient, we can use it anywhere we wish! Before we set up our OAuth callback route though, let's first examine how OAuth works. First we need to make up our link to the Google OAuth for our backend. Here we have a premade route that has the oauth ID inserted in for you already (click here to find out more about customising your OAuth URL):
To get this route to work, you'll want to create a Router that layers an axum::Extension
, then nest it onto your main router:
Once we allow the application to use our user's credentials, Google will fire a GET request to our chosen OAuth redirect URI as seen in the homepage router, with some URI query parameters. Although there's multiple parameters returned, for us we only need the code response given back by Google so we can exchange it for an access token. We can make a struct to extract the query parameters:
Then we need to exchange the code for a token by using our BasicClient
we made earlier:
The token returned by exchanging the token holds all of the information from the response and has methods to get all of the required fields that we need (life duration of the access token, the code, etc...).
Because we requested OpenID privileges earlier, we can now access any of Google's OpenID using the access token that was given to us (that we required permissions for, as per the redirect URI). Thankfully, this is pretty simple to do without the oauth2 crate and we only need to use a simple Reqwest
client with bearer auth to get the user profile data, like so:
As you can see, we needed to create a struct for the type. This particular OIDC endpoint returns much more than just an email and you'll be able to see that if you use .text().await.unwrap()
instead of trying to convert the response to JSON - however, for our purposes currently we only need the email for verification - serde
ignores unknown fields (unless deny_unknown_fields is enabled),
so this is safe to do.
Using OAuth with Axum Extensions
Now that we've got our access token, all we need to do is store the token somewhere our service can access it. We can do this with SQLx and usage of the PrivateCookieJar
type from axum_extra
, which uses cryptographically secure cookies. Let's have a look at what the code would look like:
Now that we've done everything, we want to make sure to include our token addition in the response and a redirect:
Of course, our "protected" route doesn't actually exist yet - we'll create it in a moment. But first of all, let's see what the final OAuth callback handler looks like:
To be able to authenticate users more easily, we will implement FromRequest
for UserProfile
. This will allow us to directly call the database while extracting the body. We then return the user profile of the person who just authenticated.
Now we just need to add the protected route!
Now that we've filled out everything we need, we can come back to the main entrypoint function and fill back in all of our routes so that we can use them:
Deploying to Production
Once we're done implementing OAuth, all you need to do is use shuttle deploy
(with --allow-dirty
if you're working on a dirty Git branch) and it'll work!
Finishing Up
Thanks for reading! I hope you enjoyed this guide to implementing OAuth in Rust and leveraging the oauth2 library for Rust auth.
Some extra ideas if you'd like to extend this article:
- Silent token rotation
- Add more functionality so users don't have to go through the whole OAuth process every single time
- Try implementing refresh tokens (make sure they're implemented securely!)