leaonline:oauth2-server

v4.0.0Published 2 years ago

Meteor OAuth2 Server

Test suite CodeQL built with Meteor JavaScript Style Guide Project Status: Active – The project has reached a stable, usable state and is being actively developed. GitHub

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