Read time ~ 3 coffees ☕☕☕
I recently started working on a project which allows Reddit users to schedule posts for multiple subreddits. To post something on behalf of the user, many services support the OAuth2 protocol for authorization. Reddit does too. Reddit supports 3 types of applications under this flow. The one we are interested in is the web app one. Refer to this doc if you are confused about choosing the right type.
Create an application
If you have created an application in Reddit, then you can skip this step.
Head over to https://www.reddit.com/prefs/apps
Click on the button to create a new app.
Fill in the details and hit create an app. Few things to note.
- The app is of type web type.
- Redirect URI is important so please make a note of it before saving. I generally put something like
http://localhost:3000/login/callback
there.
Congrats, you successfully created an application. 🎉
Before we move on let’s first try to understand the flow. This will help us understand the significance of redirect_uri
The OAuth Flow
The diagram above describes the OAuth flow. Let’s go over the flow once and then dive into the code.
- The user visits the Web Application and clicks on the “Login With Reddit” button.
- The user is then taken to Reddit and is asked to either deny or approve the permissions requested by the application.
- If the user approves, then the user is redirected to the
redirect_uri
(Remember this?). This will redirect the user back to the web application. - The
redirect_uri
redirects the user back to the web app along with 2 things.code
andstate
. We will talk aboutstate
later. - The web app then makes requests to the Reddit API for the user’s
access_token
. Thecode
value received in the previous step is passed as a parameter in this request. - Reddit validates the
code
passed and then returns anaccess_token
and arefresh_token
. - The web app saves the
access_token
and now can make requests to the Reddit API on behalf of the user.
Some OAuth providers return an access_token after step 3. But for Reddit, we need to perform a few more steps.
The Code.
function Home(props) {
function openLogin() {
window.open(
`https://www.reddit.com/api/v1/authorize?
client_id=YOUR_CLIENT_ID&response_type=code
&state=random-string
&redirect_uri=http://localhost:3000/login/callback
&duration=permanent&scope=identity,submit,save`,
"_self"
)
}
return (
...
<Button onClick={() => {openLogin()}}>Login With Reddit</Button>
....
)
}
This is a react component. But it can be anything. The important thing is the user should be redirected to Reddit after clicking the button.
A few things to note.
- Here, we redirect the user to the
/authorize
endpoint. We pass a few query parameters. Let’s go over them quickly.client_id
This is the string that you can see below the name of the application we created in step 1.response_type
This needs to be set tocode
You can read more on this here.state
This is a random string that you pass to the endpoint. This same string is returned later when Reddit redirects the user to theredirect_uri
. This field is important to understand if the web app has received a response to a request that it initiated.redirect_uri
This should match with the one in step 1.duration
This defines for how long do you need theaccess_token
. Allaccess_token
expire after 1 hour. More on this here.scope
This defines the permissions the web app needs, to function properly. A list along with their description of each scope is available here https://www.reddit.com/api/v1/scopes
The user will be presented with the permission screen like below if everything goes right.
Users can either Allow or Decline access to the web application.
Let’s consider the happy path ie. user allows access. In both cases, the user is redirected to redirect_uri
function LoginWithReddit({location}) {
let {search} = useLocation()
useEffect(() => {
const query = new URLSearchParams(search);
axios.get(`http://localhost:3003/login_reddit?code=${query.get("code")}`)
})
return (
<Flex>
<CircularProgress isIndeterminate color="orange"/>
</Flex>
)
}
// This is how react router is setup.
<BrowserRouter>
<Routes>
<Route path="/login/callback" element={<LoginWithReddit/>} />
<Route path="/" element={<Home/>} />
</Routes>
</BrowserRouter>
This is the react component that gets rendered when the user is redirected back to the web app.
Things to note
- The
code
received in the query parameter is sent to the back end. The back end then requests the Reddit API to get the necessary tokens. - An alternative approach could be to redirect the user to the back-end directly, which will save an API call.
Back-end server code
... // express server boilerplate
app.get("/login_reddit", async(req,res) => {
try {
// GET USER ACCESS TOKEN
const code = req.query.code;
const encodedHeader = Buffer.from(`${process.env.REDDIT_CLIENT_ID}:${process.env.REDDIT_CLIENT_SECRET}`).toString("base64")
let response = await fetch(`https://www.reddit.com/api/v1/access_token`, {
method: 'POST',
body: `grant_type=authorization_code&code=${code}&redirect_uri=http://localhost:3000/login/callback`,
headers: {authorization: `Basic ${encodedHeader}`, 'Content-Type': 'application/x-www-form-urlencoded'}
});
let body = await response.json();
// GET USER INFORMATION
response = await fetch(`https://oauth.reddit.com/api/v1/me`, {
method: "GET",
headers: {authorization: `bearer ${body.access_token}`}
})
let user = await response.json();
await signInUser(user['id'],user['name'],body['access_token'],body['refresh_token'])
let token = await signJWT(user['id'])
res.send({token})
} catch(e) {
res.send({msg: "Something went wrong."})
}
})
... // other endpoints
This is a code snippet from an express server that exposes GET /login_reddit
endpoint.
Things to note
Getting user tokens POST /api/v1/access_token
- Reddit uses HTTP Basic Authentication for fetching user access and refresh tokens. This authentication mechanism requires that the client sends base64 encoded credentials in the
Authorization
header inBasic [base64 encoded credentials here]
format. - 3 things need to be passed in the body here
grant_type
This needs to be set toauthorization_code
code
This needs to be set to the value returned by Reddit after redirecting the user to theredirect_uri
redirect_uri
I don’t know why we need to pass this again 🤷🏻
- Along with the
Authorization
header, theContent-Type
header needs to be set toapplication/x-www-form-urlencoded
This is super important. You might also need to setUser-Agent
. This is generally the name of your application. - The response finally returns the tokens. An
access_token
and arefresh_token
. Access tokens are valid only for 1 hour. To get a freshaccess_token
,refresh_token
is used.
Getting User Information GET api/v1/me
- To get user information, the request needs to be made to Reddit OAuth URL. So instead of
https://www.reddit.com
request needs to be sent tohttps://oauth.reddit.com
Please make sure you are sending all your requests (for protected endpoints) to this URL. Otherwise, you will end up with 401 Unauthorized responses. - The
Authorization
header is set to theaccess_token
that was sent in the above step. Reddit follows the bearer schema which means the header should be set to a value of the formatbearer [access_token_here]
Logging in the user
- Once the user information is fetched from Reddit. A JWT can be created and sent back to the browser which will then log in the user into the web application.
‼️ JWT isn’t secure. It is just a base64 encoded string. Storing sensitive information like passwords in these tokens could pose a huge security risk.
To re-issue access tokens after they have expired can be done by calling /api/access_token
It is pretty straight forward and more information can be found here. There can be a few strategies to get fresh access tokens.
- A cron job can be set up that checks and fetches fresh access tokens for the ones that have expired. The downside I can see to this approach is this will fetch fresh access tokens for accounts that are inactive thus wasting precious API requests. Also as the app scales, a single cron job might not be sufficient.
- Fresh access tokens can be fetched on-demand. If Reddit API returns an expired access token error then a fresh access token can be fetched. This ensures that requests are made only for active users.
And that’s it. The web app can now call protected Reddit API endpoints on behalf of the user.
Comment if you have any questions. You can follow me on Twitter. I usually tweet about JavaScript.
If you are learning React then you might be interested in Writing useState hook from scratch.
I also make YouTube videos. Go, check them out!