epfl:accounts-oidc Atmosphere package
Connect your Meteor application to an identity provider (IdP) using the modern and popular OpenID-Connect (OIDC) protocol.
Features
- Fully compatible with the Meteor accounts API
- Supports the OIDC “Authorization Code Flow”, in both “popup” and “redirect” flavors¹
¹ 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
- PKCE (RFC7636)
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-oidcassumes 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
emailfield of theUserInfoIdP JSON response against the.services.oidc.idfield of existing users, to avoid creating duplicates; - creates and populates new users'
.profile.given_nameand.profile.last_nameat first login; - updates the
.services.oidc.claimsfrom 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 name | Purpose | Example value(s) | Default |
|---|---|---|---|
loginStyle | Choose the UX for the login operation in the browser | "popup" or "redirect" | "popup" |
scope | A list of strings (with IdP-specific meaning) stipulating which personal information to retrieve at login time | "openid email" or ["openid", "email"] | "openid" |
loginUrlParameters | IdP-specific additional query parameters to pass along with the initial browser redirect to the IdP | {"foo": "bar"} | {} |
baseUrl | The base URL to resolve OpenID-Connect endpoints from | https://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/v2.0 | N/A |
tokenEndpoint | The URL of the OIDC Token Endpoint | https://login.microsoftonline.com/aaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeeeee/oauth2/v2.0/token | token_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration" |
authorizeEndpoint | The 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_info | authorization_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration" |
userinfoEndpoint | The 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_info | userinfo_endpoint JSON response field at URL: baseUrl + "/.well-known/openid-configuration" |