Skip to content
Home » Implementing Single Sign-On (SSO) with SAML: A Quick Guide

Implementing Single Sign-On (SSO) with SAML: A Quick Guide

Implementing Single Sign-On (SSO) provides a seamless login experience for users, allowing them to access multiple applications using one set of credentials. In this blog, we’ll walk through an example of implementing SSO using SAML (Security Assertion Markup Language) in NestJs.

Furthermore, this guide includes sample controller and service code, which effectively handles both the SSO login and the authentication process.

SSO-blog-image-1

What is SSO?

SSO is an authentication method that enables users to log in to multiple applications with a single set of credentials. Also, it improves the user experience by reducing the number of login prompts and enhances security by centralizing the authentication process.

What is SAML?

SAML is a widely used protocol for implementing SSO, and it facilitates the exchange of authentication and authorization data between a service provider (your application) and an identity provider (IdP). Also, in a typical SAML SSO flow, the service redirects the user to the IdP for authentication. Once the user logs in successfully, the service grants access to its resources.

Understanding the SAML SSO Process

In a typical SAML SSO flow:

  1. The user attempts to access a protected resource in your application.
  2. The application redirects the user to the identity provider (IdP) for authentication.
  3. After successful authentication, the IdP sends a SAML response to the application.
  4. The application verifies the response and grants the user access.

Controller Implementation

The controller handles two key routes for SSO:

  1. GET /sso-login: Firstly, initiates the SSO process by redirecting the user to the IdP for authentication.
@Get('sso-login')
@Redirect(samlConfig.redirect, 302)
async ssoLogin(@Request() req: any) {
  return {
    statusCode: 302,
    url: samlConfig.redirect
  };
}
  • ssoLogin method is responsible for initiating the SSO login process and it returns an object containing a url and a statusCode.
  • The URL returned in the response serves as the redirect URL, which points to the Identity Provider (IdP) configured in samlConfig.redirect. Consequently, when the user accesses this endpoint, the application sends a 302 redirect response. As a result, the user is seamlessly guided to the IdP for authentication.
  • The status code 302 indicates a temporary redirect, meaning the user will be redirected to the provided URL (the IdP login page) to continue the authentication flow.
  • The user authenticates on the identity provider’s side, such as with a corporate login system, and is redirected back to the application along with a SAML response that includes the authentication token.
  1. POST /sso: Secondly, you receives the SAML response after authentication, processes the data, and issues an access token.
@Post('sso')
@Render('login')
async ssoAuthPost(@Body() body) {
  const data = await this.authService.handleSSO(body);
  return data;
}
  • The /sso route handles the SAML response sent by the IdP after the user logs in. It passes the response body to the handleSSO method in the service for validation and processing.

Service Implementation of Single Sign-On (SSO)

The service is where the SAML response is validated, and user data is processed. Also, it performs several key functions:

  1. Firstly, validates the SAML response.
  2. Secondly, fetches or creates a user based on the response.
  3. Thirdly, issues an access token and processes user invites and permissions.

Service Code:

async handleSSO(body: any) {
  let message = 'Error in SSO';
  const issuer = '';
  
  try {
    let options = {
      cert: fs.readFileSync(`./config/saml-cert.txt`, 'utf-8'),
      issuer
    };

    const saml = new SAML(options);
    const result = await saml.validatePostResponseAsync(body);

    let user = null;
    let authRespEmail = result?.profile?.Email;
    let authRespName = `${result.profile?.FirstName || ''} ${result.profile?.LastName || ''}`;

    if (authRespEmail) {
      user = await this.authRepository.findOne({
        where: { uniqueId: authRespEmail },
      });
    }

    if (user) {
      // Update auth token if user already available.
      user.auth_token = makeid(24, 'au');
    } else {
      // Create a new user with validated user details.
      user = {
        id: makeid(16, 'au'),
        name: authRespName,
        uniqueId: authRespEmail,
        auth_token: makeid(24, 'au'),
        password: '',
        status: 'ACTIVE',
      };
    }

    user = await this.authRepository.save(user);
    const accessToken = this.jwtService.sign({ uuid: user.auth_token });
    
    // * Retrieve the user invitation data to ensure the user has already been invited.
    // * Update the invitation status and user permissions accordingly.

    }

    return {
      success: true,
      message: 'SSO Login Success, you are being redirected to the application ',
      accessToken
    };

  } catch (ex) {
    console.log('SSO AUTH EXCEPTION:: ', ex);
    message = ex.message;
  }

  return {
    success: false,
    message,
  };
}

Key Functionality:

  1. SAML Validation: The handleSSO method uses the SAML library to validate the SAML response. First, it reads the certificate from the configuration file to ensure secure communication. Then, await saml.validatePostResponseAsync(body) validates the response body using the specified certificate and issuer options.
  2. User Authentication: Also, if the user already exists in the database, the system updates their authentication token. Otherwise, it creates a new user with default credentials and stores the user in the database.
  3. Access Token Generation: Also, the system generates a JWT access token based on the user’s updated authentication token, which can be used for future API requests.
  4. User Invite and Permissions: Additionally, the service checks if the user was previously invited to the system. Also, if an invite exists, it updates the user’s status and assigns the appropriate permissions.

The @Render('login') decorator in this context ensures that after processing the SSO authentication, a corresponding login view is rendered to show the result to the user.

When the ssoAuthPost method returns a value (in this case, the data object), this data will be passed to the login template.

Inside the login view, we can reference this data to dynamically render the page. For example, if data contains user details, the template can display those details on the page.

This provides a smooth user experience by giving feedback on the SSO process, whether it succeeded or failed.

Conclusion

In conclusion, Implementing SAML-based SSO can greatly enhance the application’s security while simplifying the user experience. Hence, this example demonstrates how to set up SSO using SAML, process the authentication response, and manage user information efficiently. Also, for more information and resources, visit ceegees.in.