Adds restricted-access state and autologin links to alanning:roles
JavaScript
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
client
common
server
tests
.versions
CHANGELOG.md
LICENSE
README.md
package.js

README.md

Meteor package that adds a restricted-access state and autologin links to alanning:roles.

The main use case is sending an email or sms to your user with a link to your app that contains an OTP (one-time password) that automatically logs them in (so they don't have to enter their username/password or do OAuth):

Josh Owens just commented on your blog post:
https://my-blog-app.com/post/abc?comment=3?token=A10F51nigkFsShxmvkLnlQ76Kzjh7h9pMuNxpVpO81a

If you want the user to be fully logged in, use the package loren:login-links. If you want the user to be temporarily logged in with restricted permissions, use this package. The login is temporary - it only lasts for the duration of the DDP connection (it uses login-links connectionLogin). You can use both packages together, as long as you use different type names and call LoginLinks.setTypes before Roles.setRestrictionTypes.

Contents

Basic usage

  1. Configure your restriction types.
  2. Generate an access token.
  3. Put it in a secure HTTPS URL and send it to the user via email, sms, etc.
  4. When the user clicks the link, get the token from the URL and use it to log in the user.

On server

alice.roles // ['user', 'admin']

Roles.setRestrictionTypes({
  userOnly: {roles: ['user']}
});

token = Roles.generateRestrictedAccessToken(alice, {type: 'userOnly'});

Email.send({
  text: 'Click this: https://myapp.com/autologin/' + token,
  ...
});

You could also use the token for all your emails to users, adding it as a query parameter that works on any route:

text: 'Josh Owens just commented on your post: https://myapp.com/anyroute?foo=bar&token=' + token

Then on client

if (! Meteor.userId()) {
  token = get token from URL (depends on your router and link format)

  Roles.restrictedLogin(token, function(e, r) {
    if (!e) {
      alice = Meteor.user();
      Roles.userIsInRole(alice, ['user']); // true
      Roles.userIsInRole(alice, ['admin']); // false
    }
  });
}   

Configuration

Restrictions

You can configure restrictions using types or on a per-token basis.

The roles a user has in a restricted state is the intersection of the restricted roles list you configure and their normal roles (user.roles). For example in the below scenario, the restricted user would only have the user role.

// alice.roles is ['user', 'admin']

Roles.generateRestrictedAccessToken(alice, {roles: ['user', 'editor']});

Expiration

When a login is attempted with a token that is expired, a 'login-links/token-expired' error will be thrown. The default token expiration is one day.

You can configure expiration in three ways:

  • globally: Roles.setDefaultExpirationInSeconds(60 * 60); // one hour (call from both client and server)
  • per type
  • per token

Changes to Roles package

The restricted roles are used for Roles.userIsInRole(user, roles, [group]) when user is the logged-in user, as well as Roles.getRolesForUser(user, [group]) and the isInRole UI helper. They are not used for getUsersInRole or getGroupsForUser - those and all other functions remain unchanged from the base Roles package.

Restriction information is stored on the DDP connection. When you check roles inside of methods, it's easy for roles-restricted to access the connection. However, when you check roles outside of a method context, you must pass the context as a final parameter.

  • userIsInRole(user, roles, [group], [context])
  • getRolesForUser(user, [group], [context]

For instance inside of a publish function:

Meteor.publish('data', function() {
  Roles.userIsInRole(user, ['editor'], null, this)
  Roles.userIsInRole(user, null, this)
})

If you don't have a publish context, then you can use {unrestricted: true} to do a check that assumes the user is unrestricted, or you can create an object to pass in:

{
  userId: String
  connection: {
    _roles: {
      unrestricted: true
    }
    OR
    _roles: {
      restrictedRoles: {
        roles: ['user']
        group: 'group1'
      }
    }
  }
}

The roles will be restricted if user matches context.userId and context.connection._roles.unrestricted is not true.

Security note

See login-links security note.

API

setRestrictionTypes

Roles.setRestrictionTypes({
  typeName1: {
    roles: ['role1', 'role2', ...]
    group: 'group1' // if you use groups
    expirationInSeconds: 10 * 60 // optional
  },
  typeName2: ...
});

Roles.generateRestrictedAccessToken(alice, {type: 'typeName1'});

Using types is optional. If used, call from both server and client.

generateRestrictedAccessToken

Roles.generateRestrictedAccessToken(user, opts) (server)

  • user: userId or user object
  • opts:
    • type: String
    • roles: [String]
    • group: String (if you use groups)
    • expiresInSeconds: Integer (optional)

opts must include either type or roles. The token is 43 alphanumeric characters, and only the hashed version is stored in the DB.

restrictedLogin

Roles.restrictedLogin(token, cb) (client)

See login-links connectionLogin

isInRoleWhenUnrestricted

{{isInRoleWhenUnrestricted role group}} (client)

Template helper, analagous to isInRole, but ignores any current restriction.

Advanced

onResumeAttemptCompleted

Roles.onResumeAttemptCompleted(cb) (client)

The cb function is provided a boolean argument loggedIn (whether the resume attempt was successful).

In the basic example, we check Meteor.userId() at load time before doing a restrictedLogin - if the user is already logged in, we don't need to do a restricted login.

if (! Meteor.userId())
  Roles.restrictedLogin(token, cb);

Meteor.userId() is optimistically set by Meteor at pageload when Meteor is in the process of doing a resume login. In some cases - if the resume token has expired or been removed from the database (for instance by Meteor.logoutOtherClients), then the resume login will fail, and Meteor.userId() will be set to null. To handle these cases, you can do the following:

token = // get from URL

if (Meteor.userId()) {
  Roles.onResumeAttemptCompleted(function(loggedIn) {
    if (! loggedIn) 
      Roles.restrictedLogin(token, cb);
  });
} else {
  Roles.restrictedLogin(token, cb);
}  

removeResumeAttemptCompletedHook

var hook = Roles.onResumeAttemptCompleted(fn)
Roles.removeResumeAttemptCompletedHook(hook)

isUnrestricted

Roles.isUnrestricted()

See whether the current connection is in a restricted state or not.

From inside a publish function, instead use Roles.isUnrestrictedFromPublish(this).

restrict

Roles.restrict(opts)

opts must include either type or roles.

Manually place the connection in a restricted state. When called on server, it only applies to the server side of the connection. When called on the client, it is applied to both sides.

Note that this only lasts for the duration of the connection, so if they have a valid resume token (localStorage.getItem('Meteor.loginToken')), the user will be unrestricted on reconnect, on reload, or in other browser tabs.

Package dev

ES6 without semicolons

Testing

git clone git@github.com:lorensr/roles-restricted.git
cd roles-restricted
meteor test-packages ./
open localhost:3000

Credits

Thanks to Share911 for sponsoring 👏 share911.com – the best emergency response system for your organization.

Contributors