leaonline:oauth2-server

v4.0.0Published 4 years ago

This package has not had recent updates. Please investigate it's current state before committing to using it in your project.

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