NestJS Authentication: Single Sign On with SAML 2.0

Andrea Cioni
Towards Dev
Published in
6 min readSep 11, 2022

--

When we talk about web applications Single Sign On (SSO) plays a strategic role in user authentication.

Introduction

Nowadays SAML2 is not the first choice when we deal with this kind of arguments but it has its important role in most enterprise contexts. Because of that and based on the growing popularity of Nest framework, I decided to write this tutorial in order to guide anyone who wish to authenticate its users using SSO through SAML2 protocol.

Before starting I suggest you to take a look at the official documentation because is a great starting point if you are going to integrate any authentication mechanism on top of your application.

The Big Picture

What we will try to accomplish here is the login of a user already registered to a third-party Identity Provider (IdP) to our Service Provider (SP). We will receive all the information about users directly from there once login succeed. Here is the authentication flow that will be implemented later in details:

User authentication flow

To simplify some aspects of this integration these assumptions were made:

  • SP is an application that serves both static and dynamic content.
  • Sessions are not discussed nor used in this tutorial (feel free to ask in the comments if you’d like to see a dedicated article).
  • IdP setup is not discussed, we are going to use samltest.id as as the easiest choice (you just need to upload your SP metadata and then you are good to go).
  • We require the SAML response to be signed. No encryption or request signing.
  • HTTP-Redirect SAML request
  • HTTP-POST binding
  • Service Provider initiated
  • Single Log Out (SLO) not discussed

Requirements

Packages versions:

  • NestJS: v9.0.0
  • Passport: v0.6.0
  • Passport SAML: v3.2.1
  • Passport JWT: v4.0.0

Logging In

The first step of every authentication is your users navigating to your home page and click on a login button.

In NestJS you could do so by creating a specific endpoint in the app.controller.ts that expose a simple web page that mimics the behavior of a more sophisticated web app.

static resource serving
Our ultra-basic web application

The static page could display two states based on the current authentication status of the user:

  • not authenticated: display “Log In” button; click on that button will call another Nest endpoint ( /api/auth/sso/saml/login ) that is responsible to launch the SSO process. We’ll get there in a moment.
  • authenticated: display profile information and a “Log Out” button; we said that we won’t deal with sessions or SLO so clicks on this link will just delete user token from localStorage .

At this point, if you start Nest and try to access the page you’ll get:

user is not logged in

Login button is there but is not going to do anything until we implement the logic behind it.

Handling user login request on NestJS

Like any other authentication method we will use authentication guards (auth guards) and strategies to handle this task.

Strategy

Strategies are the Nest way to interact with Passport. Nest has a special function, PassportStrategy , that you should use to wrap end extend Passport authentication strategies. This is done pretty easily by creating the following provider:

SamlStrategy is the core of everithing

In the strategy constructor you configure the Passport specific parameters. These parameters are the same you should have passed to the vanilla passport-saml :

  • callbackUrl: the full path of the assertion consumer endpoint
  • cert : certificate used to validate the SAML response signature
  • entryPoint: the URL of the endpoint of the IdP that will handle our SAML request
  • issuer: a.k.a Service Provider Id, this must match the one provided to your IdP
  • wantAssertionSingned : we want to receive a signature in the

A more detailed and exhaustive documentation of all the possible parameters can be found here .

Authentication Guards

Auth guards, on the other hand, are special type of Guards that interact with our strategy in order to secure the endpoint on which they are applied.

SAML authentication guard

At the end we have all the ingredients to write our login function:

Login endpoint

Nothing is, apparently, done in this route because all the logic to build the SAML request and user redirection to IdP landing page, is transparently handled by our SamlAuthGuard .

If you restart Nest you should now be redirected to IdP login page once “Log In” have been pressed:

IdP SSO login page

However, if you try to access, the actual login can’t succeed because we are still missing the last (and the biggest) piece of the puzzle.

Consuming SAML assertions

Here we are, the core part of the SAML authentication workflow is done at this stage. Our IdP, after a successful login, will send back to our application the response containing the profile of our user.

But wait… what’s a user?

User concept may vary between IdP and SP perspective. For example on IdP you could have just user email and name. On the other hand, SP must need to associate more data to a user profile (i.e. country, phone number, etc… )

User concept may vary from SP and IdP perspective

Said that we need something between IdP and SP that “translate” from one to another. You could have noticed inside saml.strategy.ts a method called validate that takes in input a Profile and outputs a User .

That’s what I was talking about!

validate gets called with the assertion ( Profile ) from IdP and contains all the available user information. With them we are able to build a User that represent an entity on our SP domain.

As the name suggest “validate” do not have just this conversion responsibility. You may have some additional logic here to accept or not a supplied user (attributes format validation, call other systems, etc…).

Profile definition in passport-saml showing all possible attributes

Don’t be scared to see some information like username and phone retrieved from unexpected profile attributes. This is good, in fact, attribute definition depends on the configuration of your IdP (samltest.id, in my case).

At this point the only remaining thing is to add the endpoint to our controller dedicated to assertion consuming:

Assertion consumer endpoint definition

This route is protected by the same SamlAuthGuard that we saw for the login. However the responsibility of this route is completely different. This endpoint gets called by IdP through a POST redirect that contains the SAML assertion containing the user profile information. passport-saml detects the input and handle the response for us. In case of a positive assertion the content of the response is converted to a Profile object and passed for further processing to validate function.

Please note that the endpoint here api/auth/sso/saml/ac must be the same one you specified in your strategy path parameter.

With a valid instance of User our job is almost done. In the assertion consumer URL we:

  • store the whole User on SP side (with the help of UserService)
  • issue a JWT token (using AuthService )
  • redirect user to the web app with the token as a query parameter
{
"sub": "morty",
"iss": "https://samltest.id/saml/idp",
"iat": 1662903567,
"exp": 1662903627
}
AuthService use User information to build and sign a JWT token
UserService implementation store users in-memory

Expose user profile

At this point all the heavy lifting is done. What is still missing is an endpoint protected with a different guard ( JWTGuard ) that will retrieve the user information based on the data available in the token.

It could worth point out that in this route we are using JwtAuthGuard . This route is different and it’s completely unrelated to SAML or SSO. Because of that we need to setup another strategy that will build User based on JWT content.

For this task we need to use UserService (previously used to store the same information) to retrieve a valid User instance based on sub value.

At this point everything is ready to give it a try. Start Nest and try to login. If everything is correct you should land here:

Finally landed on user profile

Bonus Tip: Service Provider Metadata

In some cases it may be necessary to provide a Service Provider metadata file to the IdP in order to setup the federation between the two parts. In this case we can expose such information with a dedicated, unguarded, route in the app.controller.ts :

Conclusion

Full working example is available on GitHub .

I hope you’ve find this article useful. If you liked it please leave some claps and if you have any question feel free to leave a comment. I’d love to hear from you!

Thanks for reading!

--

--

Software Developer 💻 | Cloud Engineer ☁️ | Mountain hiker 🏔 | Pizza specialist 🍕