Sylvain Lemoine
Another dev blog in the nexus…

Spring Security SAML2.0 Websso with Angular client

· by Sylvain Lemoine · Read in about 6 min · (1167 Words)
Java Spring Spring security Angular Saml2.0

After my previous post Spring SAML2.0 Websso and JWT for mobile API some dev asked me about how to use saml authentication with an angular App or more generally a Single Page Application (SPA). I wrote a sample project on this github repo one or two weeks ago. This topic provides additional information about it.

The code snippets provided here take the above mentioned post as a starting point. I recommend you to read my previous post about SAML2.0 WebSSO authentication with spring-security-saml before. If you’re in a hurry, you’re also free to directly jump to the sources of the saml-angular sample repo.

We’ll introduce slight modifications in the previous code base to allow SAML authentication from SPA. Here is a sum up of the expected authentication flow:

  1. User agent loads the SPA.
  2. SPA makes an Ajax request to some «give me an authentication token (JWT)» to the backend authentication service.
  3. As the user is currently not authenticated, the backend will reply with a 401 - Unauthorized http status response.
  4. SPA opens the saml login page on a new tab e.g. https://myhost/mybackend/saml/login.
  5. User logs in on the company’s identity provider (IdP) web page.
  6. IdP redirects saml authentication response to the backend Service Provider (SP). We assume that the user has been successfully authenticated.
  7. A JSession is now established between the service provider and the user agent.
  8. The backend redirects the user to the SPA page.
  9. SPA makes again an Ajax request to some «give me an authentication token (JWT)» authentication backend service.
  10. As the user is now fully authenticated, the service replies with the authentication JWT.
  11. SPA stores the JWT in browser storage (like localstorage).
  12. SPA puts the jwt in some header at each API calls.

Steps 1 to 4

On the backend

The authentication REST controller code responsible for supplying JWT to the authenticated user can be written as:


@RestController
@RequestMapping("/auth")
public class AuthController {

  @GetMapping("/token")
  public ApiToken token() throws JOSEException {

    final DateTime dateTime = DateTime.now();

    //build claims
    JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder();
    jwtClaimsSetBuilder.expirationTime(dateTime.plusMinutes(120).toDate());
    jwtClaimsSetBuilder.claim("APP", "SAMPLE");

    //signature
    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), jwtClaimsSetBuilder.build());
    signedJWT.sign(new MACSigner(SecurityConstant.JWT_SECRET));

    return new ApiToken(signedJWT.serialize());
  }
}

The above REST service, as all the other application services is protected by the SAML security configuration (SamlSecurityConfig class). This is not compliant with the third statement:

3. As the user is currently not authenticated, the backend will replies with a 401 - Unauthorized http status response.

As any saml protected resource, the attempt to access the resource will result in a 301 (or 302) html redirects toward SAML identity provider login page (SSOSircle). As this is not the behavior we need here, we will put this service out of the SAML Security protection scope but still make it inaccessible for unauthenticated user.

Let’s review the SecurityConfig class and add the AuthSecurityConfig class

/**
 * @author slemoine
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * Rest security configuration for /api/
     */
    @Configuration
    @Order(1)
    public static class RestApiSecurityConfig extends WebSecurityConfigurerAdapter {
        /** the jwt config for /api/** services */
    }

    /**
     * Rest security configuration for /auth/token
     */
    @Configuration
    @Order(2)
    public static class AuthSecurityConfig extends WebSecurityConfigurerAdapter {

        private static final String apiMatcher = "/auth/token";

        @Override
        protected void configure(HttpSecurity http) throws Exception {

            http
                    .exceptionHandling()
                    .authenticationEntryPoint(new Http401AuthenticationEntryPoint("SAML2.0 - WEBSSO"));

            http.antMatcher(apiMatcher).authorizeRequests()
                    .anyRequest().authenticated();
        }
    }

    /**
     * Saml security config
     */
    @Configuration
    @Import(SamlSecurityConfig.class)
    public static class SamlConfig {

    }

}

The path /auth/token will now replies 401 for every unauthenticated user and the only way to be authenticated is through SAML login.

On the angular client

From the client point of view, steps 1,2,3,4 are a straight-forward flow of execution. Call the token API, receive the 401 status code response, pass the http response through the handleTokenError method which then opens the saml login endpoint. The saml endpoint redirects to the identity provider login page thus starting the saml authentication process.

ngOnInit(): void {

    this.httpClient.get('/service/auth/token')
      .subscribe(
        r => this.handleTokenSuccess(r as ApiToken),
        error => this.handleTokenError(error));

  }

  handleTokenSuccess(apiToken: ApiToken) {
    // not ready for the moment
  }

  handleTokenError(error: HttpErrorResponse) {

    if (error.status === 401) {
      window.location.replace('http://localhost:8080/saml/login');
    }
  }

Steps 5 to 7

Not covered here, it’s SAML2.0 WebSSO and spring-security-saml basic stuff.

Step 8

The backend redirects the user to the SPA page. To achieve this, will make use of one special parameter of rhe SAML2.0 RFC: RelayState. see http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html.

The RelayState is a parameter passed forth and back between the service provider (our backend) and the identity provider. In our case, it will contains the url to which the user agent will be redirected when the authentication process has been successfully terminated.

You can set it through the WebSSOProfileOptions bean in SamlSecurityConfig:

  @Bean
     public WebSSOProfileOptions defaultWebSSOProfileOptions() {
         WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
         //... other things
         webSSOProfileOptions.setRelayState("https://mywebsite");
         return webSSOProfileOptions;
     }

or you can create your own SAML entry point if you need to interacts with the original login request. For instance, with the following entrypoint, you can allow the client to choose where it should be redirected by calling /saml/login?redirectTo=https://mywebsite

@Bean
     public SAMLEntryPoint samlEntryPoint() {
        SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
        SAMLEntryPoint samlEntryPoint = new SamlWithRelayStateEntryPoint();
        samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
        return samlEntryPoint;
     }
public class SamlWithRelayStateEntryPoint extends SAMLEntryPoint {

    @Override
    protected WebSSOProfileOptions getProfileOptions(SAMLMessageContext context, AuthenticationException exception) {

        WebSSOProfileOptions ssoProfileOptions;
        if (defaultOptions != null) {
            ssoProfileOptions = defaultOptions.clone();
        } else {
            ssoProfileOptions = new WebSSOProfileOptions();
        }

        //Do some stuff with the context parameter
        //You can get the request from context and extract some parameters
        HttpServletRequestAdapter httpServletRequestAdapter = (HttpServletRequestAdapter)context.getInboundMessageTransport();

        String myRedirectUrl = httpServletRequestAdapter.getParameterValue("redirectTo");

        if (myRedirectUrl != null) {
             ssoProfileOptions.setRelayState(myRedirectUrl);
        }

        return ssoProfileOptions;
    }
}

So far, we have set the go i.e. the setup handled by spring-security-saml before sending the authentication request with the relay state to the identity provider. Let’s set the back, when the authentication response from the IdP is received by spring-security-saml:

   @Bean
     public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
         SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler = new SAMLRelayStateSuccessHandler();
         return successRedirectHandler;
     }

Almost nothing to do, as spring-saml-security already provides the SAMLRelayStateSuccessHandler class which is a Spring Security success handler devoted to redirect the user agent to the content of the RelayState parameter.

Steps 9 to 12

As the Spring Security context is now established with the authenticated user, the call to the /auth/token endpoint should be successful. Just grab the token and store it on some browser storage.

  ngOnInit(): void {

    this.httpClient.get('/service/auth/token')
      .subscribe(
        r => this.handleTokenSuccess(r as ApiToken),
        error => this.handleTokenError(error));

  }

  handleTokenSuccess(apiToken: ApiToken) {
    this.apiToken = apiToken.token;
    localStorage.setItem("apiToken", apiToken.token);
    this.callApi();
  }

  handleTokenError(error: HttpErrorResponse) {

    if (error.status === 401) {
      this.showUnauthorizedMessage = true;
      setTimeout(() => window.location.replace('http://localhost:8080/saml/login'), 4000);
    }
  }

  callApi() {
    const apiToken = localStorage.getItem("apiToken");

    this.httpClient.get('/service/api/mycontroller/', {
      headers: {
        "x-auth-token": apiToken
      }
    }).subscribe(r => this.apiResult = JSON.stringify(r));
}


The callApi() method above is just a call to ensure that the REST api be used with the JWT.

Special note

Beware that if you handle multiple backend nodes, you’ll probably need to handle session replication to make sure that the user can retrieve the JWT from any node.

Thanks for reading

Hope this post can give you some leads to integrate SAML authentication with your SPA.

sample sources: https://github.com/slem1/saml-angular

Comments