Implementing Serverless OAuth
for JAM Stacks and static sites
- tags
- static_sites
- functions
- firebase
Contents
Most of the serverless platforms have their own forms of authentication, but it might not support the specific service that you are looking to use. Lets go through how we can build a react single page app, hosting on firebase, that talks to the unsplash service directly. It will be hosted on firebase stoage, and with a tiny bit of firebase functions to tie it together.
How oauth works
Here is the overall process:
There are 3 entities – the user’s browser, our server, and the third party service.
- On user action, such as pressing a Log in With Google button, a request is made to your server.
- A signed request is generated which includes the access levels that we wish to be granted. The client ID and client secret, which are provided by the service provider (under something like Create An App) are used to sign this request so that the server knows which app is requesting permission.
- The browser is redirected to the third party service with this request.
- The user interacts with the third party service, and grants us the permission.
- The browser is redirected to the “call back url”, which is generally needs to be white listed at the same place you got your client id and secrets.
- The authcode is then used by our server to request an access_token from the service. authcode are very short lived so prevent play back attacks since they are exposed to the browser. Access tokens are longer lived and aren’t generally exposed to the browser.
- The server (optionally) gets information for the account, for example name and avatar photos, to configure a local account.
- A logged in version of the site is returned to the user’s browser.
In this scenario:
- The user’s password is never seen by our server.
- The user doesn’t need to remember yet another password for your site.
- The access token is never seen by the user.
- Access Tokens can be revoked by the third-party service at any time.
- Access Tokens can be tied to a developer, so bad actors on the server level can be locked out.
- The third party service can grant fine grained control of what data they grant access to.
What do we need to implement
- A client ID from the third party service.
- A client secret from the third party service.
- An auth method to contruct the initial requests.
- An auth_callback method to process the final request.
- An auth callback url that is granted access by the third party service’s app configuration.
The first 2 are created at a third party service. For this example, I’m going to choose firebase to host our functions and website, and Unsplash since it’s a pretty fun third-party service that’s a bit off the beaten path and its not too hard to get an app registered.
Create the app on the third party service
Go ahead and follow that the unsplash link and register as a developer.
Then press the big New Application
button and verify that you are going to follow the rules. Give your application a name and a quick description, then create!
Note your Application Key and Secret. This could aslo be called your client id and secret. We’ll need those later. Set your callback url to https://localhost:3000/auth_callback
. Once we deploy to firebase we’ll need to go back with our actual URL, but this is what we’ll use fore testing.
Create our javascript template
We are going to use create-react-app
and then going to add firebase to it.
|
|
Setup firebase
Now we setup our “server” on the FireBase console Create a new application. Select the web integration, and copy the example config into src/firebaseConfig.js
, something like below:
|
|
If you haven’t already, it’s time to install the firebase-tools
page. I’m using yarn here:
|
|
And then lets add firebase to our project:
|
|
This will first prompt you to login to firebase so your machine has development credentials. Then init
will configure your local project with the correct configuration.
- Select at least functions and hosting.
- Choose the application that you just created.
- I chose JavaScript instead of TypeScript.
- Yes to ESLint.
- Yes to install dependencies.
build
as the hosting directory.- Yes to single page app.
- No to overwriting
index.html
if asked.
Firebase should now be configured. Lets do a quick test of the deploy.
|
|
And open your browser to the url that was firebase deploy
spit out at the end. If you see a blank screen, double check that in firebase.json
you have build
set as the public
directory in hosting.
Set the secrets for your firebase functions
We are going to store the oauth secrets inside of firebase to keep them seperate from your code. These are the application id and application secret that we got from unsplash above that you should have taken note off. Lets set those in firebase now (and be sure you enter in your own secrets!)
|
|
We can then pull these secrets back down locally into a firebase env file so that when we are testing out our firebase code it will behave like it will in production.
|
|
We don’t want these files to go into git so
$ echo functions/.runtimeconfig.json >> .gitignore
Setup firebase functions
Lets go into the functions
directory and add a few npm modules. One for the http, and the other for the oauth flow.
|
|
Now lets create a simple app that we can use to test out our install. Replace functions/index.js
with:
|
|
Then run firebase serve --only functions
to start up the api locally. Be sure to check out the url that the proxy code is running on. In my case, it’s http://localhost:5001/honey-b6642/us-central1/oauth
. Once this is running you should be able to go to the url listed and see the client id from the configuration.
Setup the react app
We need to point our react code to our firebase functions, and we are going to put that information into env
files so that there is one place to swap them out later. These need to be prefaced with REACT_APP_
in order to play well with the create-react-app
build process. Create a .env.development
file with your information in it
REACT_APP_BASE_URL="http://localhost:5001/honey-b6642/us-central1/oauth"
We also don’t want this file in source control:
|
|
Once we deploy the server to fire base, we can point this to our production instances. Also, we will create another .env.production
file for build time information. Note that we don’t want the oauth secrets here, since this is for the JavaScript code, not for the server functions.
First lets add some small styling to that our eyes won’t hurt during development.
|
|
Rename src/index.css
to src/index.scss
, and include the bootstrap sass files. This is a bit overkill at the moment, but it will set things up for easy customization going forward.
|
|
Be sure to update src/index.js
to point to the correct style sheet, change index.css
to index.scss
Now we can build out a scaffolding for src/App.js
:
|
|
Now start up the server using yarn start
, and press the Connect
button. You should now see the response from the firebase function running locally!
Implementing the OAuth Flow
Now that all the pieces are in place, it’s time to start implementing the oauth logic inside of the firebase functions.
First lets add a few libraries to the functions/package.json
:
$ cd functions
$ yarn add simple-oauth2 randomstring
Also edit the local functions/.runtimeconfig.json
to include the callback url that we are going to pass to unsplash. Mine looks like:
|
|
Now lets write some code! Lets replace what we have in functions/index.js
with a method to create a authorization request to unplash and then redirect the user’s browser to it.
|
|
First we setup simpleOauth
. We are using the configuration id and secret that we got from unsplash, and the endpoints that are specified in the documentation for the token
and authorize
path. When we get a /auth
request we use create an authorization request using that configuration and then redirect the browser to it.
We haven’t written a callback handler yet, but lets try it out. First go to unsplash and log out of your account to see the whole flow. Then go to http://localhost:3000 and press the auth button. You’ll be prompted to log in, and when you do you should see:
If you don’t, a couple of things to check
- Is your client id and secret being set correctly?
- Is your redirect_uri configured correctly?
- Are you passing the correct scope? (In this case,
public
)
OK, now it’s time to write the callback method!
|
|
When you press “Grant Access” on Unsplash, it will generate an authorization code and redirect the user’s browser back to this function. Here we use that code to get an access token from unsplash that we can then use to make requests on the user’s behalf.
Using the token
In the scenario we outlined at the top, the server would store the access_token and connect to the server with authenticated requests from there. In the case of firebase, we’d great something like an Anaonymous account, store the token there, and then proxy requests from the server to the third party service never exposing the access_token to the browser. This is a clean way to do this so that the access_token is never exposed.
But it makes it more complicated, so we are going to hack through it for demo purposes to just finish up the demo. The code that we are writing is going to connect to the unsplash api from the browser directly, so we’ll change our fireback function code to redirect back to our static app with an url that contains the access_token.
So, lets have the code redirect back to our react site with the query string as the parameter.
First lets add where we’d like it to go to the configuration in functions/.runtimeconfig.json
:
|
|
Then lets change our function to redirect instead of spitting out json. So in functions/index.js
change:
|
|
to:
|
|
Now we need to update our react code so that it knows what to do with that access token. Lets first add a node module that understands how to parse query strings:
|
|
And replace the App class in src/App.js
with the following:
|
|
We need to create that Unsplash
component, so lets do that now inside of src/Unsplash.js
(and don’t forget to import
this at the top of App.js
!)
|
|
The first thing that we are doing is creating a utility function that uses the fetch
function and passes the Bearer
token, a/k/a access_token
to the api. In the componentDidMount
method of the Unsplash
class, we use that function to initiate the request. Once we get that data, we show the rather bare bones Profile
component that has a name and a picture in it.
Proof of concept: working locally!
Deploying everything to firebase
Now lets package this up to run on production. First we need to tell the react app where the firebase functions are. Go to the FireBase Console, select your project, and find the functions admin panel on the left side. See where the are deployed. Mine is https://us-central1-honey-b6642.cloudfunctions.net/oauth
, so in .env.production
lets add:
REACT_APP_BASE_URL="https://us-central1-honey-b6642.cloudfunctions.net/oauth"
Now lets kick off a production build of the react app:
|
|
Now we need to setup the config for the firebase functions. Lets make sure that we have an entry for everything that is in our functions/.runtimeconfig.json
file. First set the static_site_url value to the url you are looking at, in my case https://honey-b6642.firebaseapp.com/
|
|
For the redirect_uri, use the same as the REACT_APP_BASE_URL
above but add /callback
. Mine is https://us-central1-honey-b6642.cloudfunctions.net/oauth
, so
|
|
Once that is done, lets push both the html/css/js code as well as the functions to firebase:
|
|
If you have the problem listed here, update functions/package.json
to downgrade simple-oauth2
to ^1.6.0
, return yarn
in the functions
directory, and retry the firebase deploy.
Finally, we need to whitelist our callback URL inside of the Unsplash Application. Follow that link, select your application, and add that same callback url from above after the localhost one that should already be there.
Now lets test it out! Make sure that you have billing enabled in FireBase, otherwise you won’t be able to make external requests and you will get a host not found error.
Final thoughts
The code is available on GitHub here: https://github.com/wschenk/unsplash_api_firebase
We’ve covered a lot of ground here!
- Overview of the OAuth protocol
- Hosting a static create-react-app site on firebase
- Building firebase functions and testing them locally
- Environment config with create-react-app in production and development
- Environment config with firebase functions, also in production and development
- Using
simple-oauth2
- A quick and easy way to pass the access_token back to our react app
- How to use the
Bearer
token usingwindow.fetch
to get authenticated data from an API.
References
- https://github.com/wschenk/unsplash_api_firebase
- https://firebase.google.com/docs/functions/get-started
- https://firebase.google.com/docs/functions/local-emulator
- https://www.netlify.com/blog/2018/07/30/how-to-setup-serverless-oauth-flows-with-netlify-functions--intercom/
- https://github.com/Herohtar/netlify-cms-oauth-firebase
- https://unsplash.com/documentation
- https://github.com/unsplash/unsplash-js
Previously
Next