loading...
Cover image for Serverless OAuth Proxy

Serverless OAuth Proxy

simov profile image simo ・7 min read

Let's talk about OAuth! And more specifically 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:

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

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:

{
  "defaults": {
    "transport": "state"
  }
}

With State Transport the response 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:

{
  "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

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:

{
  "defaults": {
    "transport": "querystring"
  }
}

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

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

Grant will redirect to that callback path or absolute URL, and encode the response data as querystring.

This transport is useful when using Grant as 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:

{
  "$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

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:

{
  "defaults": {
    "transport": "session"
  }
}

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

{session: {secret: 'grant'}}

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

{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:

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 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:

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 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:

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.

Similar to AWS Lambda, the Google Cloud Function needs the full path prefix that includes your function name:

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

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:

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, 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, azure, gcloud, vercel

Happy Coding!

Posted on by:

simov profile

simo

@simov

(λ (λ (λ (λ (λ (⌐■_■))))))

Discussion

markdown guide