epfl:accounts-oidc

v0.1.0Published 3 weeks ago

epfl:accounts-oidc Atmosphere package

Connect your Meteor application to an identity provider (IdP) using the modern and popular OpenID-Connect (OIDC) protocol.

Features

¹ It is a bit unfortunate that the Meteor terminology sometimes uses “popup flow” and “redirect flow”, while they are one and the same flow in the OpenID-Connect sense 🤷‍♂️ In this documentation, we use “login style” instead.

Planned Features

Non-features

  • Support for Meteor versions prior to 3
  • “Older” OpenID-Connect or OAuth flows (implicit, hybrid, and so on)
  • Client-side OAuth token redeeming. epfl:accounts-oidc assumes that the server, not the browser, will be fetching the tokens from the IdP at the end of a successful authentication. That is, you should not set “single-page Web app” mode in Entra, in spite of what you believe you know about Meteor and single-page Web apps. This is in contrast to, say, @epfl-si/react-appauth.

Install

In your Meteor v3 project, say

meteor add epfl:accounts-oidc

Configure the Identity Provider

The goal of this step is to obtain the client ID, client secret and OIDC base URL for use below. Consult the documentation of your IdP to find out how to do that.

The OIDC base URL is the one that returns JSON when you paste it into your browser's URL bar, append /.well-known/openid-configuration at the end of it, and press Enter. If your IdP doesn't provide such an auto-configuration JSON document, you will have to use advanced configuration (documented below) to provide each OAuth 2 entry point by hand.

For security reasons, many OIDC-compliant IdPs, including Keycloak and Entra, want to know in advance (i.e. whitelist) which URLs the user's browser can be redirected to after logging in. Meteor doesn't let you pick the URL here; as documented, you need to use $ROOT_URL/_oauth/oidc where $ROOT_URL is the root URL of the Web app.

Configure the Meteor app

Do one of the following:

  • in your settings.json:

    1{ // ...
    2  "packages": {
    3    "service-configuration": {
    4      "oidc": {
    5        "loginStyle": "redirect",
    6        "baseUrl": "login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/",
    7        "clientId": "CLIENT-ID",
    8        "secret": { "clientSecret": "CLIENT-SECRET" }
    9      }
    10    }
    11  }
    12}
  • OR in some file under server/:

    1import { Meteor } from "meteor/meteor"
    2import { ServiceConfiguration } from "meteor/service-configuration"
    3
    4Meteor.startup(async () => {
    5  await ServiceConfiguration.configurations.upsertAsync(
    6    { service: "oidc" },
    7    {
    8      $set: {
    9        loginStyle: "redirect",
    10        "baseUrl": "https://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/",
    11        "clientId": "CLIENT-ID",
    12        "clientSecret": "CLIENT-SECRET"
    13      },
    14    }
    15  );
    16});

Use in your app

Client-side API

The sole entry point for client code is OIDC.login(); it takes no arguments. (There is no logout function; use Meteor.logout() instead.)

If, for example, you use React and react-meteor-data, your login / logout widget could look like this:

1import React from "react"
2import { Meteor } from "meteor/meteor"
3import { useTracker } from 'meteor/react-meteor-data'
4import { OIDC } from "meteor/epfl:accounts-oidc"
5
6function LoginLogoutClicky () {
7    const isLoggedIn = useTracker(() => !! Meteor.userId());
8
9    return <>
10      { isLoggedIn ?
11          <a href="#" onClick={() => Meteor.logout()}>Logout</a> :
12          <a href="#" onClick={() => OIDC.login()}>Login</a> }
13    </>;
14}

User synchronization

OIDC.getNewUserData and OIDC.getUserServiceData are two functions that the server-side application code may override, so as to customize how information is synchronized from the IdP responses into the Meteor.Users MongoDB collection on the server.

The default implementation, which will suit most needs,

  • matches the email field of the UserInfo IdP JSON response against the .services.oidc.id field of existing users, to avoid creating duplicates;
  • creates and populates new users' .profile.given_name and .profile.last_name at first login;
  • updates the .services.oidc.claims from the claims in the JWT ID token upon each successful login.

Note that epfl:accounts-oidc does not check the JWKS signature on said JWT token, because it doesn't need to. Read up on that, as well as the API that lets you alter the aforementioned default behavior, in the JSDoc comments inside index.ts in the module's source code.

RBAC example with the groups claim

Assuming your IdP is configured to disclose a groups claim in the JWT token, here is how to securely use it on the server:

1export async function getGroups () {
2  const user = await Meteor.userAsync();
3  return user?.services?.oidc?.claims?.groups || [];
4}
5
6export async function ensureMemberOfGroup (groupName) {
7  if ( -1 == (await getGroups()).index(groupName) ) {
8    throw new Meteor::Error("Unauthorized")
9  }
10}

Use await ensureMemberOfGroup("relevant-group"); as an opener in all your server-side publications, methods etc. per the recommendations of the Meteor documentation) to achieve a rudimentary, yet effective form of role-based access control, or RBAC.

Configuration Reference

Option namePurposeExample value(s)Default
loginStyleChoose the UX for the login operation in the browser"popup" or "redirect""popup"
scopeA list of strings (with IdP-specific meaning) stipulating which personal information to retrieve at login time"openid email" or ["openid", "email"]"openid"
loginUrlParametersIdP-specific additional query parameters to pass along with the initial browser redirect to the IdP{"foo": "bar"}{}
baseUrlThe base URL to resolve OpenID-Connect endpoints fromhttps://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/v2.0N/A
tokenEndpointThe URL of the OIDC Token Endpointhttps://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/oauth2/v2.0/tokentoken_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration"
authorizeEndpointThe URL of the Authorization Endpoint (the one that the server calls to finish the OAuth login process)https://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/oauth2/v2.0/user_infoauthorization_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration"
userinfoEndpointThe URL of the UserInfo Endpoint (the one that returns the signed JWT token)https://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/oauth2/v2.0/user_infouserinfo_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration"