Serverless OAuth Proxy
simo
Posted on September 1, 2020
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 theredirect
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 theredirect
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 booleantrue
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:
{
"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:
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 booleantrue
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 September 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.