Serverless OAuth Proxy
Tags: oauth, javascript, serverless, devops
Published: 2020-09-01 13:00:02

---
title: Serverless OAuth Proxy
published: true
description: OAuth Everywhere
tags: oauth, javascript, serverless, devops
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/arvnduuej8mls1un0zgy.png
---

Let's talk about OAuth! And [more][OAuth Simplified] [specifically][OAuth Like a BOSS] let's talk about **[Grant]**:

> A completely transparent **OAuth Proxy** that supports **200+** Social Login providers.

Being able to have a fully functional OAuth Client with just a few lines of code is great. However, up until recently **[Grant]** was assuming that you have an HTTP Server up and running with either Express, Koa, Hapi or Fastify on top.

And while you can get a Virtual Machine on the cheap, or spin up your own server instances, that still implies a few things about your architecture:

- You are either hosting an HTTP server already, and so attaching Grant to certain routes is not an issue
- Or you are willing to host just Grant as a standalone OAuth Client / Proxy Server

But what if your actual API consists of Serverless Functions only? Do you still have to host Grant as a separate HTTP server?

> **NO!**

**[Grant]** now comes with **4** Serverless Function handlers for:

- [AWS Lambda][examples-aws]
- [Azure Function][examples-azure]
- [Google Cloud Function][examples-gcloud]
- [Vercel][examples-vercel]

In this article we'll go over 4 different examples, covering 4 different topics in Grant. Each example will be covered in a different Serverless Function handler, either `aws`, `azure`, `gcloud` or `vercel`, but all topics and examples apply to any other Grant handler. Including the conventional HTTP Framework based ones such as `express`, `koa`, `hapi` and `fastify`.

So I encourage you to read through the entire article even if you are interested in only one Serverless Function provider. Each section will also cover the minimum amount of details that you want to know about that specific provider.

At any given point you can jump right into the examples by following the links above. All examples are using [Terraform] for reproducible deployments with a [Makefile] on top for orchestration, but you can use any other method to deploy your infrastructure.

# AWS Lambda

```js
var grant = require('grant').aws({
  config: {/*Grant configuration*/}, session: {secret: 'grant'}
})

exports.handler = async (event) => {
  var {redirect, response} = await grant(event)
  return redirect || {
    statusCode: 200,
    headers: {'content-type': 'application/json'},
    body: JSON.stringify(response)
  }
}
```

The first example is about using the State Transport in Grant:

```json
{
  "defaults": {
    "transport": "state"
  }
}
```

> With State Transport the [response data][callback-data] will be available in the `response` object returned by the Grant handler. In all other cases when Grant needs to perform an HTTP redirect, you have to return the `redirect` object instead.

One specific thing about [AWS Lambda] sitting behind [AWS API Gateway] is that it is required to specify the full path `prefix` that includes the stage name of your AWS API Gateway:

```json
{
  "defaults": {
    "origin": "https://[id].execute-api.[region].amazonaws.com",
    "prefix": "/[stage]/connect"
  },
  "google": {}
}
```

Then we login by navigating to:

```
https://[id].execute-api.[region].amazonaws.com/[stage]/connect/google
```

And the redirect URL of your OAuth App have to be set to:

```
https://[id].execute-api.[region].amazonaws.com/[stage]/connect/google/callback
```

# Azure Function

```js
var grant = require('grant').azure({
  config: {/*Grant configuration*/}, session: {secret: 'grant'}
})

module.exports = async (context, req) => {
  var {redirect} = await grant(req)
  return redirect
}
```

With [Azure Functions] we are going to take a look at the Querystring Transport instead:

```json
{
  "defaults": {
    "transport": "querystring"
  }
}
```

When Querystring Transport is being used, specifying a `callback` route or absolute URL is required:

```json
{
  "google": {
    "callback": "/hello"
  },
  "twitter": {
    "callback": "/hi"
  }
}
```

> Grant will redirect to that `callback` path or absolute URL, and encode the [response data][callback-data] as querystring.

This transport is useful when using Grant as [OAuth Proxy][dynamic-oauth-proxy], or in cases when you want to handle the OAuth response data in another lambda function. Note that this transport may leak private data encoded as querysrting in your `callback` route.

> Similar to AWS Lambda, when the `redirect` object is being returned by the Grant handler you need to return that object or do something else. Since Grant is going to redirect always when the Querystring Transport is being used, you only need to return the `redirect` object, because the response data will be handled somewhere else.

One specific thing about Azure is that it is required to set the following `requestOverrides` for the Grant handler:

```json
{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "oauth": {
      "matchCondition": {
        "route": "{*proxy}"
      },
      "requestOverrides": {
        "backend.request.querystring.oauth_code": "{backend.request.querystring.code}",
        "backend.request.querystring.code": ""
      },
      "backendUri": "http://localhost/{proxy}"
    }
  }
}
```

Azure uses the `code` querystring parameter to authenticate users. That same `code` querystring parameter, however, is also being used by the OAuth2.0 framework. That's why it is required to map the `code` parameter to `oauth_code` instead, and unset the `code` parameter, so that we can pass through the Azure's authentication layer. Then the Azure handler for Grant will map the `oauth_code` back to `code` so that it can be processed correctly.

# Google Cloud Function

```js
var grant = require('grant').gcloud({
  config: {/*Grant configuration*/},
  session: {secret: 'grant', store: require('./store')}
})

exports.handler = async (req, res) => {
  await grant(req, res)
}
```

With [Google Cloud Functions] we're going to take a look at the Session Transport:

```json
{
  "defaults": {
    "transport": "session"
  }
}
```

Up until now all of our examples were using the built-in Cookie Store:

```js
{session: {secret: 'grant'}}
```

Note that we're now specifying a `store` key as well:

```js
{session: {secret: 'grant', store: require('./store')}}
```

This will instruct Grant to use an external Session Store implementation in place of the built-in Cookie Store one:

```js
var request = require('request-compose').client

var path = process.env.FIREBASE_PATH
var auth = process.env.FIREBASE_AUTH

module.exports = {
  get: async (sid) => {
    var {body} = await request({
      method: 'GET', url: `${path}/${sid}.json`, qs: {auth},
    })
    return body
  },
  set: async (sid, json) => {
    await request({
      method: 'PATCH', url: `${path}/${sid}.json`, qs: {auth}, json,
    })
  },
  remove: async (sid) => {
    await request({
      method: 'DELETE', url: `${path}/${sid}.json`, qs: {auth},
    })
  },
}
```

This is an example implementation of using [Firebase] as external Session Store. The required methods to implement are `get` and `set`. All methods receive a Session ID, and the `set` method additionally receives an object that needs to be stored in the Session Store.

> When Cookie Store is being used all of the data that Grant needs to persist in order to execute the OAuth flow will be stored in the Browser Cookie as Base64 encoded string. Although being signed those values can be read and potentially may leak private data.

> The Cookie Store is still a valid option for certain cases, but you have to understand what values are going to be encoded there depending on the transport being used in Grant, and any potential [Dynamic Overrides][dynamic-http] that you might be using.

With Session Transport specifying a `callback` route is optional. In case you have one, it will be used to redirect the user to another lambda to handle the response:

```js
var Session = require('grant/lib/session')({
  secret: 'grant', store: require('./store')
})

exports.handler = async (req, res) => {
  var session = Session(req)

  var {response} = (await session.get()).grant
  await session.remove()

  res.statusCode = 200
  res.setHeader('content-type', 'application/json')
  res.end(JSON.stringify(response))
}
```

> Unlike Querystring Transport the [response data][callback-data] won't be encoded in the URL as querystring, and therefore it cannot leak private data.

Note that in this case we're accessing the internal session module directly:

```js
require('grant/lib/session')
```

The reason why is because Grant have nothing to do in the `callback` route. That's also the place where the session can be destroyed if needed.

> Unlike AWS Lambda and Azure Function, the Grant handler for Google Cloud Function handles the HTTP redirects internally. The `redirect` variable is still being returned by the handler when Grant is going to perform an HTTP redirect, but its value is set to a boolean `true` instead.

You have to specify the `redirect_uri` explicitly because the actual request URL contains the lambda name in the path, but that is never sent to your lambda handler:

```json
{
  "defaults": {
    "origin": "https://[region]-[project].cloudfunctions.net"
  },
  "google": {
    "redirect_uri": "https://[region]-[project].cloudfunctions.net/[lambda]/connect/google/callback"
  }
}
```

Then we login by navigating to:

```
https://[region]-[project].cloudfunctions.net/[lambda]/connect/google
```

And the redirect URL of your OAuth App have to be set to:

```
https://[region]-[project].cloudfunctions.net/[lambda]/connect/google/callback
```

# Vercel

Lastly, we're going to take a look at the Dynamic State Overrides using [Vercel]:

```js
var grant = require('grant').vercel({
  config: require('./config.json'),
  session: {secret: 'grant', store: require('../store')}
})

module.exports = async (req, res) => {
  if ('/connect/google' === req.url) {
    var state = {dynamic: {scope: ['openid']}}
  }
  else if ('/connect/twitter' === req.url) {
    var state = {dynamic: {key: 'CONSUMER_KEY', secret: 'CONSUMER_SECRET'}}
  }

  var {response, session} = await grant(req, res, state)

  if (response) {
    await session.remove()
    res.statusCode = 200
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify(response))
  }
}
```

> Configuration can be loaded dynamically and passed to the Grant handler as `state` object.

Dynamic State Overrides are useful alternative to the [Dynamic HTTP Overrides][dynamic-http], in cases when you want to configure Grant dynamically with potentially sensitive data that you don't want to send over HTTP.

Note, however, that any Dynamic Override configuration set on login is being stored in the session, so using external Session Store when overriding sensitive configuration is highly recommended.

> Similar to the Google Cloud Function handler, the Grant handler for Vercel handles the HTTP redirects internally. The `redirect` variable is still being returned by the handler when Grant is going to perform an HTTP redirect, but its value is set to a boolean `true` instead.

Lastly, all Serverless Grant handlers also return the `session` instance, used to manage the state that Grant stores in the Cookie or Session Store.

# Conclusion

With all of the Serverless Compute offerings out there we can have our API running in no time and pay for what we use only.

Using **[Grant]** on top of **any** of those Cloud Providers can give us Social Login to **any** OAuth Provider basically for free.

All Serverless handler examples can be found here: [aws][examples-aws], [azure][examples-azure], [gcloud][examples-gcloud], [vercel][examples-vercel]

Happy Coding!


  [Grant]: https://github.com/simov/grant
  [examples-aws]: https://github.com/simov/grant-aws
  [examples-azure]: https://github.com/simov/grant-azure
  [examples-gcloud]: https://github.com/simov/grant-gcloud
  [examples-vercel]: https://github.com/simov/grant-vercel

  [OAuth Simplified]: https://dev.to/simov/oauth-simplified-2pbd
  [OAuth Like a BOSS]: https://dev.to/simov/oauth-like-a-boss-2m3b

  [Terraform]: https://www.terraform.io
  [Makefile]: https://en.wikipedia.org/wiki/Makefile
  [Firebase]: https://firebase.google.com/

  [AWS API Gateway]: https://aws.amazon.com/api-gateway/
  [AWS Lambda]: https://aws.amazon.com/lambda/
  [Azure Functions]: https://azure.microsoft.com/en-us/services/functions/
  [Google Cloud Functions]: https://cloud.google.com/functions/
  [Vercel]: https://vercel.com/docs/serverless-functions/introduction

  [connect-prefix]: https://github.com/simov/grant#connect-prefix
  [callback-data]: https://github.com/simov/grant#callback-data
  [dynamic-oauth-proxy]: https://github.com/simov/grant#dynamic-oauth-proxy
  [dynamic-http]: https://github.com/simov/grant#dynamic-http