Sylvain Lemoine
Another dev blog in the nexus…

Spring SAML2.0 Websso and JWT for mobile API

· Read in about 15 min · (3120 Words)
Java Spring Saml2.0 Websso Jwt

The aim of this article is to give readers some hints and samples about Spring SAML2.0 Websso authentication integration for mobile application.

SAML 2.0 authentication is a pretty old standard (2005), and it does not really fit mobile app design well as it relies on http redirection. Nevertheless, sometimes you have to make it works according to your company security standard.

The api security described in this tutorial will feature the following security configuration:

  • Secure mobile API urls (/api/**) with JWT (json web token)
  • Secure all url but mobile api with SAML 2.0 WEBSSO
  • A SAML protected url (/auth/login) will allow users to retrieve their JWT thus enabling access to mobile API urls.

This article is split in two parts:

  • How to set up SAML2.0 WEBSSO authentication with Spring security saml (java based)
  • How to set up JWT authentication for mobile app authentication

Sources

You can get a full code sample from my github: https://github.com/slem1/saml-jwt-sample

Frameworks and libraries

I use the following frameworks and libraries

  • spring-boot
  • spring-security
  • spring-security-saml2-core
  • nimbus-jose for jwt

Current version of Spring-security-saml2 is xml-only based configuration and… I really dislike XML configuration…

but thanks to Vincenzo De Notaris, we have really good sample of how to do a java based configuration of spring security saml 2.0 at https://github.com/vdenotaris/spring-boot-security-saml-sample

Identity provider

To make SAML authentication works you need an identity provider (IdP). We will use the same free IdP as Spring Security SAML official reference documentation: https://www.ssocircle.com/en/

Project setup dependencies

We use the following maven dependencies:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security.extensions</groupId>
        <artifactId>spring-security-saml2-core</artifactId>
        <version>1.0.2.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.2.2.RELEASE</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Security setup

Let’s create the main security configuration class.

We split the security configuration between two HttpSecurity configuration, one for the secured mobile API (/api/**) secured with JWT and the other with SAML.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * Rest security configuration for /api/
     */
    @Configuration
    @Order(1)
    public static class RestApiSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/api/**").authorizeRequests().anyRequest().authenticated();
        }
    }

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

    }
}

In the 1st part, we will only focus on SecurityConfig.SamlConfig class.

Actually, to keep SecurityConfig clean and as SAML configuration requires a lot of bean declarations, we will not do any Saml Configuration directly in SecurityConfig.SamlConfig class but rather import an other configuration class called SamlSecurityConfig class.

So let’s create the SamlSecurityConfig class which will be responsible of all SAML bean declarations and configuration.

@Configuration
public class SamlSecurityConfig extends WebSecurityConfigurerAdapter{

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

    }
}

Spring SAML2.0 WebSSO Authentication

The SAML2.0 WebSSO authentication is pretty well described by the following sequence diagram from wikipedia.

schema

Let’s build the spring SAML security step by step from this point.

SAML Entrypoint

Users will try to access a SAML protected resource, and fail. This failure will be handled by Spring security ExceptionTranslationFilter implementation which then will hand over to saml authentication entry point thus starting the saml authentication from your app (Service Provider or SP) to the identity provider (IdP).

Add the following beans to configure entry point:

  @Bean
  public WebSSOProfileOptions defaultWebSSOProfileOptions() {
      WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
      webSSOProfileOptions.setIncludeScoping(false);
      return webSSOProfileOptions;
  }

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

The WebSSOProfileOptions bean allows you to setup some parameters of the AuthNRequest. The AuthNRequest is the request sent from SP to IdP for asking user authentication. For instance, if the IdP does not provide SingleLogout service endpoint you can force authentication from IdP each time the SP sends a new AuthNRequest:

     webSSOProfileOptions.setForceAuthN(true);

Next let’s start configuring HttpSecurity to declare which AuthenticationEntryPoint call when an authentication exception is triggered:

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

        http
                .exceptionHandling()
                .authenticationEntryPoint(samlEntryPoint());
    }

Let’s add another bean and start initializing the Saml security filter chain i.e the set of all SAML filters which can be involved in SAML operation.

    @Bean
    public FilterChainProxy samlFilter() throws Exception {
       List<SecurityFilterChain> chains = new ArrayList<>();
       return new FilterChainProxy(chains);
    }

Display service provider metadata

Spring security SAML will generate SP metadata for you according to your SAML configuration.

One filter will allow user to download the SP metadata from a specific url.

These are the metadata you will give (or upload) to the IdP.

    @Bean
    public MetadataDisplayFilter metadataDisplayFilter() {
        return new MetadataDisplayFilter();
    }

    @Bean
    public FilterChainProxy samlFilter() throws Exception {
       List<SecurityFilterChain> chains = new ArrayList<>();

       chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
               metadataDisplayFilter()));

       return new FilterChainProxy(chains);
    }

The filter is bound to the /saml/metadata url part.

In production environment you’ll probably disable this filter according to some environment property value.

Login

You can add specific login url (e.g /saml/login) to directly trigger samlEntryPoint.

    @Bean
    public FilterChainProxy samlFilter() throws Exception {
       List<SecurityFilterChain> chains = new ArrayList<>();

       chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
                metadataDisplayFilter()));

       chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
                samlEntryPoint()));

       return new FilterChainProxy(chains);
    }

Process Saml Response

When IdP gives back its authentication response (Whether successful or not) it redirects it to the SP processing response endpoint url (e.g. «/saml/SSO»). This redirection triggers the following filter bean class: SamlWebSSOProcessingFilter.

We give to this filter the proper handler to handle authentication result:

    @Bean
    public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
        return new SimpleUrlAuthenticationFailureHandler();
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
        SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
                new SavedRequestAwareAuthenticationSuccessHandler();
        successRedirectHandler.setDefaultTargetUrl("/");
        return successRedirectHandler;
    }

    @Bean
    public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
        SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
        samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
        samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
        samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
        return samlWebSSOProcessingFilter;
    }

and plug the filter to the «/saml/SSO» url.

    @Bean
    public FilterChainProxy samlFilter() throws Exception {
         List<SecurityFilterChain> chains = new ArrayList<>();

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
                  metadataDisplayFilter()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
                  samlEntryPoint()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
                 samlWebSSOProcessingFilter()));

         return new FilterChainProxy(chains);
    }

Logout

Logging out can be a two step process … or not depending on what you want to achieve (and what IdP allows you to do!)

Global logout

Global means 2 steps process: - Clear you SP Spring security context so user is no more authenticated on behalf of your app (let’s call it Spring Logout) - Terminate all SP sessions from the IdP side (SingleLogout)

Spring Logout only

As you might have already guessed SingleLogout can be overkill as it terminate all sessions from all SP (i.e multiple apps) previously authenticated by the IdP. However, you might want to only logout (Spring Logout) from your App. Moreover, some IdP may does not provide you SingleLogout endpoint at all.

Both way to logout can be configured with these beans:

     @Bean
     public HttpStatusReturningLogoutSuccessHandler successLogoutHandler() {
         return new HttpStatusReturningLogoutSuccessHandler();
     }

     @Bean
     public SecurityContextLogoutHandler logoutHandler() {
         SecurityContextLogoutHandler logoutHandler =
                 new SecurityContextLogoutHandler();
         logoutHandler.setInvalidateHttpSession(true);
         logoutHandler.setClearAuthentication(true);
         return logoutHandler;
     }

     @Bean
     public SAMLLogoutFilter samlLogoutFilter() {
         return new SAMLLogoutFilter(successLogoutHandler(),
                 new LogoutHandler[] { logoutHandler() },
                 new LogoutHandler[] { logoutHandler() });
     }

     @Bean
     public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
         return new SAMLLogoutProcessingFilter(successLogoutHandler(),
                 logoutHandler());
     }

And then map the logout url to the proper filter :

     @Bean
     public FilterChainProxy samlFilter() throws Exception {
         List<SecurityFilterChain> chains = new ArrayList<>();

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
                 metadataDisplayFilter()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
                 samlEntryPoint()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
                 samlWebSSOProcessingFilter()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
                 samlLogoutFilter()));

         chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
                 samlLogoutProcessingFilter()));

         return new FilterChainProxy(chains);
     }

Actually, the order of logout filter is pretty important here. samlLogoutFilter will be processed before samlLogoutProcessingFilter. samlLogoutFilter will check some extra url argument to decide if it should pursue with SingleLogout or not.

If you call /saml/logout?local=true, you’ll make a SP Spring logout only. Otherwise, you’ll propagate logout to IdP through SingleLogout endpoint.

HttpSecurity

Let’s get back to our HttpSecurity which should look like

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

        //redirect to samlEntryPoint if unauthicated
        http
                .exceptionHandling()
                .authenticationEntryPoint(samlEntryPoint());
    }

we should configure the following :

  • Disable csrf. As IdP will make redirect to your SP you cannot keep csrf protection as the IdP as no way to know which csrf token it should provide.
  • Add the saml filter chain you build so far.
  • Specify route security
   @Override
   protected void configure(HttpSecurity http) throws Exception {

       http
               .exceptionHandling()
               .authenticationEntryPoint(samlEntryPoint());
       http
               .csrf()
               .disable();
       http
               .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);
       http
               .authorizeRequests()
               .antMatchers("/error").permitAll()
               .antMatchers("/saml/**").permitAll()
               .anyRequest().authenticated();
   }

Metadata generation

Until now, we haven’t set any mechanism to generate our metadata. Metadata are mandatory to allow IdP know on which SP endpoint redirect to. Fortunately, Spring will generate these for you configure MetadataGenerator bean properly.

    @Bean
    public MetadataGeneratorFilter metadataGeneratorFilter() {
        return new MetadataGeneratorFilter(metadataGenerator());
    }

    @Bean
    public MetadataGenerator metadataGenerator() {
        MetadataGenerator metadataGenerator = new MetadataGenerator();
        metadataGenerator.setEntityId("SamlSampleEntityId");
        metadataGenerator.setEntityBaseURL("https://myappserver.com");
        metadataGenerator.setExtendedMetadata(extendedMetadata());
        metadataGenerator.setIncludeDiscoveryExtension(false);
        metadataGenerator.setKeyManager(keyManager());
        return metadataGenerator;
    }

    @Bean
    public ExtendedMetadata extendedMetadata() {
        ExtendedMetadata extendedMetadata = new ExtendedMetadata();
        extendedMetadata.setIdpDiscoveryEnabled(false);
        extendedMetadata.setSignMetadata(false);
        return extendedMetadata;
    }

Notes that we disable IdPDiscovery in ExtendedMetadata Bean as we already know it (SSOCircle). IdPDiscovery configuration is not covered here.

Let’as add the metadata generator filter to the HttpSecurity:

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

                http
                        .exceptionHandling()
                        .authenticationEntryPoint(samlEntryPoint());
                http
                        .csrf()
                        .disable();
                http
                        .addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
                        .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);

                http
                        .authorizeRequests()
                        .antMatchers("/error").permitAll()
                        .antMatchers("/saml/**").permitAll()
                        .anyRequest().authenticated();

                http
                        .logout()
                        .logoutSuccessUrl("/");
    }

As you may notice metadata generation requires a keyManager

     metadataGenerator.setKeyManager(keyManager());

keyManager will be responsible to encrypt our saml assertion sent to IdP. Let’s configure it:

    @Bean
    public KeyManager keyManager() {
        ClassPathResource storeFile = new ClassPathResource("/saml-keystore.jks");
        String storePass = "samlstorepass";
        Map<String, String> passwords = new HashMap<>();
        passwords.put("mykeyalias", "mykeypass");
        return new JKSKeyManager(storeFile, storePass, passwords, "mykeyalias");
    }

You can easily create a self signed key and keystore with the jre keytool command:

    keytool -genkeypair -alias mykeyalias -keypass mykeypass -storepass samlstorepass -keystore saml-keystore.jks

Additionally, you need the following beans to ensure TLS security :

    @Bean
    public TLSProtocolConfigurer tlsProtocolConfigurer() {
        return new TLSProtocolConfigurer();
    }

    @Bean
    public ProtocolSocketFactory socketFactory() {
        return new TLSProtocolSocketFactory(keyManager(), null, "default");
    }

    @Bean
    public Protocol socketFactoryProtocol() {
        return new Protocol("https", socketFactory(), 443);
    }

    @Bean
    public MethodInvokingFactoryBean socketFactoryInitialization() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setTargetClass(Protocol.class);
        methodInvokingFactoryBean.setTargetMethod("registerProtocol");
        Object[] args = {"https", socketFactoryProtocol()};
        methodInvokingFactoryBean.setArguments(args);
        return methodInvokingFactoryBean;
    }

We have set up the most part of the spring security beans for Spring Security Saml so far.

XML parsing

Saml is XML based protocol so you need some configuration for parsing XML

    @Bean
    public VelocityEngine velocityEngine() {
        return VelocityFactory.getEngine();
    }

    @Bean(initMethod = "initialize")
    public StaticBasicParserPool parserPool() {
        return new StaticBasicParserPool();
    }

    @Bean(name = "parserPoolHolder")
    public ParserPoolHolder parserPoolHolder() {
        return new ParserPoolHolder();
    }

SAML Binding configuration

SAML Binding depends on your IdP specs. Here, according to SSOCircle we use POST and Redirect bindings. Please refer to the SAML2.0 Specifications to know more about Bindings.

    @Bean
    public HTTPPostBinding httpPostBinding() {
        return new HTTPPostBinding(parserPool(), velocityEngine());
    }

    @Bean
    public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
        return new HTTPRedirectDeflateBinding(parserPool());
    }

    @Bean
    public SAMLProcessorImpl processor() {
        Collection<SAMLBinding> bindings = new ArrayList<>();
        bindings.add(httpRedirectDeflateBinding());
        bindings.add(httpPostBinding());
        return new SAMLProcessorImpl(bindings);
    }

Others

    @Bean
    public HttpClient httpClient() {
        return new HttpClient(multiThreadedHttpConnectionManager());
    }

    @Bean
    public MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager() {
        return new MultiThreadedHttpConnectionManager();
    }

    @Bean
    public static SAMLBootstrap sAMLBootstrap() {
        return new SAMLBootstrap();
    }

    @Bean
    public SAMLDefaultLogger samlLogger() {
        return new SAMLDefaultLogger();
    }

Context provider

Context provider is especially important if your SP acts behind a reverse proxy (RP).

Behind a RP

Spring Saml enforce security by making some check between the context of your app and the redirect url return from IdP. If they don’t match you will have an issue.

   @Bean
   public SAMLContextProviderImpl contextProvider() {
       SAMLContextProviderLB samlContextProviderLB = new SAMLContextProviderLB();
       samlContextProviderLB.setScheme("https");
       samlContextProviderLB.setServerName("myserver.com");
       samlContextProviderLB.setServerPort(443);
       samlContextProviderLB.setIncludeServerPortInRequestURL(false);
       samlContextProviderLB.setContextPath("/mycontextpath");
       return samlContextProviderLB;
   }

Without RP

in that case, default SAMLContextProviderImpl instance is okay.

@Bean
    public SAMLContextProviderImpl contextProvider() {
        return new SAMLContextProviderImpl();
    }

Web SSO profile

You need some beans for configuring WebSSO profile and logout. We use default spring saml provided implementation

   // SAML 2.0 WebSSO Assertion Consumer
    @Bean
    public WebSSOProfileConsumer webSSOprofileConsumer() {
        return new WebSSOProfileConsumerImpl();
    }

    // SAML 2.0 Web SSO profile
    @Bean
    public WebSSOProfile webSSOprofile() {
        return new WebSSOProfileImpl();
    }

    // not used but autowired...
    // SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    // not used but autowired...
    // SAML 2.0 Holder-of-Key Web SSO profile
    @Bean
    public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
        return new WebSSOProfileConsumerHoKImpl();
    }

    @Bean
    public SingleLogoutProfile logoutprofile() {
        return new SingleLogoutProfileImpl();
    }

IdP metadata

IdP should provide you its own metadata to allow you to contact it. For SSOCircle you should download them from their website and provide the proper bean configuration to locate them.

    @Bean
    public ExtendedMetadataDelegate idpMetadata()
            throws MetadataProviderException, ResourceException {

        Timer backgroundTaskTimer = new Timer(true);

        ResourceBackedMetadataProvider resourceBackedMetadataProvider =
                new ResourceBackedMetadataProvider(backgroundTaskTimer, new ClasspathResource("/ssocircle-metadata.xml"));

        resourceBackedMetadataProvider.setParserPool(parserPool());

        ExtendedMetadataDelegate extendedMetadataDelegate =
                new ExtendedMetadataDelegate(resourceBackedMetadataProvider , extendedMetadata());
        extendedMetadataDelegate.setMetadataTrustCheck(true);
        extendedMetadataDelegate.setMetadataRequireSignature(false);
        return extendedMetadataDelegate;
    }

    @Bean
    @Qualifier("metadata")
    public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException {
        List<MetadataProvider> providers = new ArrayList<>();
        providers.add(idpMetadata());
        return new CachingMetadataManager(providers);
    }

Spring security

Now let’s add some classic Spring Security configuration: - AuthenticationProvider - UserDetails

AuthenticationProvider

   @Bean
    public SAMLUserDetailsService samlUserDetailsService(){
        //TODO
        return null;
    }

    @Bean
    public SAMLAuthenticationProvider samlAuthenticationProvider() {
        SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
        samlAuthenticationProvider.setUserDetails(samlUserDetailsService());
        samlAuthenticationProvider.setForcePrincipalAsString(false);
        return samlAuthenticationProvider;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         auth
                .authenticationProvider(samlAuthenticationProvider());
    }

User Details

You may want to retrieve user details according to your business logic

Let’s define a SamlUserDetails class

public class SamlUserDetails implements UserDetails {
       @Override
       public Collection<? extends GrantedAuthority> getAuthorities() {
           return new ArrayList<>();
       }

       @Override
       public String getPassword() {
           return null;
       }

       @Override
       public String getUsername() {
           return null;
       }

       @Override
       public boolean isAccountNonExpired() {
           return false;
       }

       @Override
       public boolean isAccountNonLocked() {
           return false;
       }

       @Override
       public boolean isCredentialsNonExpired() {
           return false;
       }

       @Override
       public boolean isEnabled() {
           return false;
       }
   }


And the user details service implementation which will populate these details :

public class SamlUserDetailsServiceImpl implements SAMLUserDetailsService {

    @Override
    public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException {
        return new SamlUserDetails();
    }
}

finally bind our user details service to the SAMLAuthenticationProvider

    @Bean
    public SAMLUserDetailsService samlUserDetailsService(){
        return new SamlUserDetailsServiceImpl();
    }

Your Spring Security SAML configuration should be done now !

Upload the service provider metadata

Access the metadata display filter from /saml/metadata with your browser. The metadata should be asked for download.

Send the metadata to your IdP. For SSOCircle you can upload them at https://idp.ssocircle.com/sso/hos/SPMetaInter.jsp

JWT

JWT configuration should be a piece of cake compared to the extra (extra) verbose SAML configuration

Dependency

Add the following dependency to your project

    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>4.37</version>
    </dependency>

JWT creation

Let’s create Login API endpoint to allow users to get their token.

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

    private static final String jwtSecret = "yeWAgVDfb$!MFn@MCJVN7uqkznHbDLR#";

    @GetMapping("/login")
    public ResponseEntity<String> login() throws JOSEException {

        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
        } else {

            final SAMLCredential credential = (SAMLCredential) authentication.getCredentials();

            final DateTime dateTime = credential.getAuthenticationAssertion()
                    .getIssueInstant()
                    .toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault()));


            //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(jwtSecret));

            return new ResponseEntity<>(signedJWT.serialize(), HttpStatus.OK);
        }
    }
}

In the above code, we get the SAML credential previously set by the SAML authentication filter. Then we create a JWT and give it back to the user. We tampered in the token some information, like app name and we set the token expiry date. In real app you’ll probably extract user details from the authentication object like roles and also tampered them into the token.

Filter and authenticate

Now we have to make our user access our API through JWT.

We will create a new Spring Security filter by extending the spring’s AbstractAuthenticationProcessingFilter

We only override three methods which answer to the following questions :

  • How to authenticate ?
  • What to do if authentication is successful ?
  • What to do if authentication is unsuccessful ?

How to authenticate ? We will authenticate user through custom http header which will contain our JWT. We delegate authentication to the AuthenticationManager instance.

What to do if authentication is successful ? Setup the spring security context holder and delegates to the next filter in the security filter chain

  • What to do if authentication is unsuccessful ? Clear the security context holder and give the unauthorized http response.

The following piece of code answers these demands:

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public static final String HEADER_SECURITY_TOKEN = "x-auth-token";

    public JwtAuthenticationFilter(final String matcher, AuthenticationManager authenticationManager) {
        super(matcher);
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        final String token = request.getHeader(HEADER_SECURITY_TOKEN);
        JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token);
        return getAuthenticationManager().authenticate(jwtAuthenticationToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
        chain.doFilter(request, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    }
}

In order to rely on standard Spring security mechanism through AuthenticationManager and AuthenticationProvider, we create a new instance of JwtAuthenticationToken, extending the org.springframework.security.authentication.AbstractAuthenticationToken.

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final transient Object principal;

    public JwtAuthenticationToken(Object principal) {
        super(null);
        this.principal=principal;
    }

    public JwtAuthenticationToken(Object principal, Object details, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setDetails(details);
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return "";
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }
}

then we implements AuthenticationProvider which will have to do all the necessary checks about the JwtAuthenticationToken validity. If successfully authenticated, the token will be stored in Spring SecurityContextHolder (look back to successfulAuthentication method in JwtAuthenticationFilter).

public class JwtAuthenticationProvider implements AuthenticationProvider {

    @Override
    public boolean supports(Class<?> authentication) {
        return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }

    @Override
    public Authentication authenticate(Authentication authentication) {

        Assert.notNull(authentication, "Authentication is missing");

        Assert.isInstanceOf(JwtAuthenticationToken.class, authentication,
                "This method only accepts JwtAuthenticationToken");

        String jwtToken = authentication.getName();

        if (authentication.getPrincipal() == null || jwtToken == null) {
            throw new AuthenticationCredentialsNotFoundException("Authentication token is missing");
        }


        final SignedJWT signedJWT;
        try {
            signedJWT = SignedJWT.parse(jwtToken);

            boolean isVerified = signedJWT.verify(new MACVerifier(SecurityConstant.JWT_SECRET.getBytes()));

            if(!isVerified){
                throw new BadCredentialsException("Invalid token signature");
            }

            //is token expired ?
            LocalDateTime expirationTime = LocalDateTime.ofInstant(
                    signedJWT.getJWTClaimsSet().getExpirationTime().toInstant(), ZoneId.systemDefault());

            if (LocalDateTime.now(ZoneId.systemDefault()).isAfter(expirationTime)) {
                throw new CredentialsExpiredException("Token expired");
            }

            return new JwtAuthenticationToken(signedJWT, null, null);

        } catch (ParseException e) {
            throw new InternalAuthenticationServiceException("Unreadable token");
        } catch (JOSEException e) {
            throw new InternalAuthenticationServiceException("Unreadable signature");
        }
    }
}

In a real app you’ll probably extract user details (roles…) from authentication token and set these info into the new JwtAuthenticationToken thus enabling SecurityContextHolder to keep these info.

Now let’s finish spring security http configuration for mobile API part :

 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

        private static final String apiMatcher = "/api/**";

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

            http.addFilterBefore(new JwtAuthenticationFilter(apiMatcher, super.authenticationManager()), UsernamePasswordAuthenticationFilter.class);

            http.authorizeRequests()
                    .antMatchers(apiMatcher)
                    .authenticated();
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(new JwtAuthenticationProvider());
        }
    }

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

    }
}

Mobile team job part

So how my Mobile team folks use all of this ? they open a WebView (they really hate that…) and access to /auth/login page. Then, user is redirected to IdP login page. When successfully logged in they retrieve the jwt and store it in app memory (and also close the web view).

As you may notice, return the token as the login method does just show the token directly in web page. It’s ok for the sample but in our real app we just return it using a Mustache template and an hidden field form field on the webpage.

Thanks you for reading !

Available sources at: https://github.com/slem1/saml-jwt-sample

Comments