Meteor OAuth2 Server
This package is a implementation of the package
@node-oauth/oauth2-server
for Meteor.
It can run without express
(we use Meteor's builtin WebApp
) and implements
the authorization_code
workflow and works like the Facebook's OAuth popup.
Changelog
View the full changelog in the hsitory page.
Install
meteor add leaonline:oauth2-server
Implementation
Server implementation
The following example uses the full configuration with their current default values.
server/oauth2server.js
1import { Meteor } from "meteor/meteor" 2import { OAuth2Server } from 'meteor/leaonline:oauth2-server' 3 4const oauth2server = new OAuth2Server({ 5 serverOptions: { 6 addAcceptedScopesHeader: true, 7 addAuthorizedScopesHeader: true, 8 allowBearerTokensInQueryString: false, 9 allowEmptyState: false, 10 authorizationCodeLifetime: 300, 11 accessTokenLifetime: 3600, 12 refreshTokenLifetime: 1209600, 13 allowExtendedTokenAttributes: false, 14 requireClientAuthentication: true 15 }, 16 model: { 17 accessTokensCollectionName: 'oauth_access_tokens', 18 refreshTokensCollectionName: 'oauth_refresh_tokens', 19 clientsCollectionName: 'oauth_clients', 20 authCodesCollectionName: 'oauth_auth_codes', 21 debug: true 22 }, 23 routes: { 24 accessTokenUrl: '/oauth/token', 25 authorizeUrl: '/oauth/authorize', 26 errorUrl: '/oauth/error', 27 fallbackUrl: '/oauth/*' 28 } 29}) 30 31// this is a "secret" route that is only accessed with 32// a valid token, that has been generated by the authorization_code grant flow 33oauth2server.authenticatedRoute().get('/oauth/ident', function (req, res, next) { 34 const user = Meteor.users.findOne(req.data.user.id) 35 36 res.writeHead(200, { 37 'Content-Type': 'application/json', 38 }) 39 const body = JSON.stringify({ 40 id: user._id, 41 login: user.username, 42 email: user.emails[0].address, 43 firstName: user.firstName, 44 lastName: user.lastName, 45 name: `${user.firstName} ${user.lastName}` 46 }) 47 res.end(body) 48}) 49 50oauth2server.app.use('*', function (req, res, next) { 51 res.writeHead(404) 52 res.end('route not found') 53}) 54
Additional validation
Often, you want to restrict who of your users can access which client / service. In order to decide to give permission or not, you can register a handler that receives the authenticated user and the client she aims to access:
1oauth2server.validateUser(function({ user, client }) { 2 // the following example uses alanning:roles to check, whether a user 3 // has been assigned a role that indicates she can access the client. 4 // It is up to you how you implement this logic. If all users can access 5 // all registered clients, you can simply omit this call at all. 6 const { clientId } = client 7 const { _id } = user 8 9 return Roles.userIsInRoles(_id, 'manage-app', clientId) 10})
Client/Popup implementation
You should install a router to handle client side routing indendently from the WebApp routes. You can for example use
$ meteor add ostrio:flow-router-extra
and then define a client route for your popup dialog:
client/main.html
1<head> 2 <title>authserver</title> 3</head> 4 5<template name="layout"> 6 {{> yield}} 7</template>
client/main.js
1import { FlowRouter } from 'meteor/ostrio:flow-router-extra' 2import './authorize.html' 3import './authorize' 4import './main.html' 5 6// Define the route to render the popup view 7FlowRouter.route('/dialog/oauth', { 8 action: function (params, queryParams) { 9 this.render('layout', 'authorize', queryParams) 10 } 11})
client/authorize.js
1// Subscribe the list of already authorized clients 2// to auto accept 3Template.authorize.onCreated(function() { 4 this.subscribe('authorizedOAuth'); 5}); 6 7// Get the login token to pass to oauth 8// This is the best way to identify the logged user 9Template.authorize.helpers({ 10 getToken: function() { 11 return localStorage.getItem('Meteor.loginToken'); 12 } 13}); 14 15// Auto click the submit/accept button if user already 16// accepted this client 17Template.authorize.onRendered(function() { 18 var data = this.data; 19 this.autorun(function(c) { 20 var user = Meteor.user(); 21 if (user && user.oauth && user.oauth.authorizedClients && user.oauth.authorizedClients.indexOf(data.client_id()) > -1) { 22 c.stop(); 23 $('button').click(); 24 } 25 }); 26});
client/authorize.html
1<template name="authorize"> 2 {{#if currentUser}} 3 <form method="post" action="{{redirect_uri}}" role="form" class="{{#unless Template.subscriptionsReady}}hidden{{/unless}}"> 4 <h2>Authorise</h2> 5 <input type="hidden" name="allow" value="yes"> 6 <input type="hidden" name="token" value="{{getToken}}"> 7 <input type="hidden" name="client_id" value="{{client_id}}"> 8 <input type="hidden" name="redirect_uri" value="{{redirect_uri}}"> 9 <input type="hidden" name="response_type" value="code"> 10 <button type="submit">Authorise</button> 11 </form> 12 {{#unless Template.subscriptionsReady}} 13 loading... 14 {{/unless}} 15 {{else}} 16 {{> loginButtons}} 17 {{/if}} 18</template>
client/style.css
1.hidden { 2 display: none; 3}
Testing
We use mocha with meteortesting:mocha
to run the tests. You can run the tests in watch mode via
TEST_WATCH=1 TEST_CLIENT=0 meteor test-packages ./ --driver-package meteortesting:mocha
You can also run the tests once via
TEST_CLIENT=0 meteor test-packages ./ --once --driver-package meteortesting:mocha
License
MIT, see license file