A simple OAuth client
June 29, 2014 Leave a comment
We’ve talked about OAuth in a few posts now, so I thought it might be a good idea to try some of this out for real. (My other motivation is that we’re adding OAuth to Blaze, so I promised Richard I’d have a client for when he’s ready for testing).
I decided to start with a simple web based client and see how far I got.
From a technology perspective, Javascript and Node.js is my preferred language, so I created a simple node server that is going to act as my OAuth ‘client’ which I can then use to call different OAuth servers. The overall architecture looks a bit like this:
A pretty basic web application, which serves up HTML pages to the browser, and communicates separately via HTTPS (of course!) to the OAuth servers. Because it’s a ‘secure’ application (the code runs on the server and not in the users browser) it can use the ‘authorization code’ flow (or grant).
For my library I chose to use the node module ‘simple-outh2’ which exposes simple methods to make the required OAuth calls.
The first experiment is against Google. Google uses OAuth2 to authorize access to its services – in effect Google hosts both Authorization and Resource servers in the OAuth2 model, so should be quite straight forward.
First step is to establish credentials that the client can use to Authenticate against the Authorization Server (ie the client ID and client secret), which I got from the Developer website.
Then the app is quite simple:
var request = require('request'); //https://github.com/mikeal/request var path = require('path'); var express = require('express'), app = express(); app.use(express.cookieParser()); app.use(express.session({secret: '1234567890QWERTY'})); app.use(express.static(path.join(__dirname, 'public'))); var OAuth2; var credentialsGoogle = { clientID: "<myclientid>", clientSecret: "<mysecret>", 'site': 'https://accounts.google.com/o/oauth2/', 'authorizationPath' : 'auth', 'tokenPath' : 'token' }; // Initial call redirecting to the Auth Server app.get('/auth', function (req, res) { OAuth2 = require('simple-oauth2')(credentialsGoogle); authorization_uri = OAuth2.AuthCode.authorizeURL({ redirect_uri: 'http://localhost:3001/callback', scope: 'openid email https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/tasks', state: '3(#0/!~', access_type: "offline" //causes google to return a refresh token }); res.redirect(authorization_uri); }); // Callback endpoint parsing the authorization token and asking for the access token app.get('/callback', function (req, res) { var code = req.query.code; OAuth2.AuthCode.getToken({ code: code, redirect_uri: 'http://localhost:3001/callback' }, saveToken); function saveToken(error, result) { if (error) { console.log('Access Token Error', error.message, error); res.json({'Access Token Error': error.message}); } else { //see what we've got... console.log(result); //this adds the expiry time to the token by adding the validity time to the token token = OAuth2.AccessToken.create(result); //save the response back from the token endpoint in a session req.session.token = result; //perform the res of the processing now that we have an Access Token gotToken(req,res); } } //Now the token has been received and saved in the session, return the next page for processing function gotToken(req, res) { res.sendfile(__dirname +"/public/main.html"); }; }); //get the lists for the current user... app.get("/tasks", function (req, res) { //Need a token to access the google services if (req.session.token) { var AT = req.session.token["access_token"]; var url = "https://www.googleapis.com/tasks/v1/users/@me/lists"; var options = { method: "GET", headers: { "content-type": "application/json+fhir", "authorization": "Bearer " + AT }, rejectUnauthorized: false, //to allow self-signed cetificates uri: url }; request(options, function (error, response, body) { res.json(body); }); } else { res.json({err:"Not logged in"}); } }); app.listen(3001); console.log("OAuth Client started on port 3001");
- The flow starts by calling the /auth endpoint – line 21 above – (I had a simple <a> tag in the front page). This initialises the OAuth2 object with the required credentials – and the location of the Authorization and Token endpoints, and then re-directs to Authorization Server with the required parameters (like scope, state and others). You can find the details on the Google site.
- Google then serves up a login page showing what scope you are after (eg accessing google drive and tasks) and the user logs in and authenticates the request. (If you’re logged in to google services separately it remembers who you are – which can be a bit disconcerting at first).
- Assuming all goes well, then the browser will eventually be redirected back to the local callback endpoint (http://localhost:3001/callback – line 34 in the example above) which extracts the Authorization code from the reply and requests an Authorization Token from Google.
- Once that’s done we save the token (actually the whole response from the Token request) and serve up another HTML page.
- Now, the user can access the Google resources – an example is the /task endpoint (line 67) which will get the task lists for the current user.
Interestingly, the access_type value of ‘offline’ in line 27 above is supposed to cause Google to include a Refresh Token in its response – which doesn’t seem to be happening for me, so I need to look into that. It means that when the Access Token expires, then the user should have to re-authenticate – though that didn’t seem to be happening. It may be that Google is doing something clever on the backend…
Oh, and if you look at the response that comes back from the server when you get a Authorization Token, you’ll see an id_token as well. Because we put ‘openid’ in the scope property, Google will also implement OpenId Connect and return Identity data as well.
So, as you can see, the library abstracts much of the complexity of OAuth away – and I presume that the libraries in other languages act in a similar fashion.
But – this is a rather simple flow where both Authorization and Resource servers are served up by the same app – which can then do all sorts of clever things on the back end. What happens when they are on different servers?
We’ll look in to that soon…
Recent Comments