What is the best way to implement SAML that would work with AD FS?
Hi @bladerunner2020 maybe this discussion is of any help. The package is a little old but at least it provides a connector into the accounts system of Meteor that you could (maybe) use Updated SSO - SAML v2 library
Finally, we forked node-saml and implemented SSO by ourselves )
Still would be nice to have an official or supported package for SSO.
Hi @bladerunner2020. I’m just investigating this very same topic. Can you link to your fork? What modifications did you have to make for it to work with Meteor Accounts?
We just fixed couple of things in node-saml. Here is the link
The basic idea is:
- Initialise NodeSaml:
const saml = new SAML(options);
- Form client we request authorisation URL with a unique ID:
// implemented on server
const methods = meteorMethods({
'saml.getAuthorizeUrl': async function(uniqueId: string): Promise<string | undefined> {
debug(`Generate authorize url, id = ${uniqueId}`);
try {
const url = await saml.getAuthorizeUrl({ uniqueId });
debug(`Authorize url: ${url}`);
return url;
} catch (err) {
throw toMeteorError(err);
}
}
});
- On server we create a listener for callback from IS:
WebApp.connectHandlers.use('/services/saml2', callback)
- In callback we get from body data and check them with NodeSaml:
const result = await saml.validatePostResponse(body);
let uniqueId;
if (result.success && result.profile?.inResponseTo) {
uniqueId = result.profile.inResponseTo.replace('_', '');
receivedProfiles[id] = {
timestamp: Date.now(),
...result.profile
};
}
// ....
const base = Meteor.absoluteUrl();
const redirectUrl = `"${base}auth/login-page?method=saml&uniqueId=${uniqueId}"`;
debug(`Redirect URL: ${redirectUrl}`);
res.write(`<script>window.location.replace(${redirectUrl});</script>`);
res.end();
- Client is redirect to a page that process login. To process login we use
Accounts.registerLoginHandler('saml', (options: { uniqueId: string, methodName: string }) => {
const { uniqueId, methodName } = options;
if (methodName !== 'saml') return undefined;
debug(`Trying to login with SAML. UniqueId = ${uniqueId}`);
const samlUser = receivedProfiles[uniqueId];
// Must exist, but we still to check it!
if (!samlUser) {
log.error(`SAML user not found in received profiles. Unique session ID: ${uniqueId}`);
throw new Meteor.Error('SAML user not found');
}
delete receivedProfiles[uniqueId];
const { nameID } = samlUser;
if (!nameID) {
log.error(`SAML error - no nameID property. Unique session ID: ${uniqueId}`, samlUser);
throw new Meteor.Error('SAML error - no nameID property');
}
const user = Meteor.users.findOne({
$or: [
{ email: { $regex: `^${nameID}$`, $options: 'i' } },
{ emails: { $elemMatch: { address: { $regex: `^${nameID}$`, $options: 'i' } } } },
{ username: { $regex: `^${nameID}$`, $options: 'i' } },
]
});
if (!user) {
throw new Meteor.Error(`User not found: ${nameID}`);
}
const { _id: userId } = user;
const stampedToken = Accounts._generateStampedLoginToken(); // eslint-disable-line no-underscore-dangle
const hashStampedToken = Accounts._hashStampedToken(stampedToken); // eslint-disable-line no-underscore-dangle
Meteor.users.update(userId, { $push: { 'services.resume.loginTokens': hashStampedToken } });
return {
userId,
token: stampedToken.token
};
});
This code is torn out our implementation so I removed some parts of code and put it together. So, don’t try to copy/paste it.
Thanks a lot for sharing that, much appreciated. I was looking at your fork to see what had changed and you might be able to use the new v4 of node-saml that was launched recently.
It would be cool if meteor accounts supported SAML for SSO as from the small bit I’ve read, that’s actually the way it should be handled, and not via OAuth, although no doubt OAuth is simpler.
We’ll give it a try anyway and report back here if we’ve any issues.
We just fixed a small bug and added a couple of methods we missed.