Basic spring security concepts with JWT tokens (Spring with REST controllers)

Introduction:
I just started using spring boot. To understand how it works, I tried to convert my existing project (spring MVC, JSP in frontend) to spring using a REST controller and AngularJS in the frontend.

Facing problem:
I ran into a big security issue during the migration. As I understand it, the best way to have a good security layer is now working with JWT tokens and supports oauth2, which has many posts / tutorials that provide various information even about the basics of security layer architecture.

So the question is:
Can anyone provide a complete list of the parts / class security levels required to have basic (but not welcome) security features for a spring boot application with REST controllers. Please do not suggest using stormpath : I want to implement it myself to understand better.

Reasoning about this big question here:
I did my own investigation on this topic, but I thought that most of the links I checked contain a lot of bad practices, so a flawed security layer architecture is possible. so I really would like to know some good security layer architecture design practice.


Feature Required Details: I have a standard list of features that I want to support.

  • oauth2 support (but also be able to authenticate without it)
  • registration request (create jwt token and return to client)
  • login request (get jwt token if user was registered)
  • logout request (release the jwt token)
  • token timeout
  • multiple roles
  • leisure controllers that do authentication and authorization (could you please provide an example piece of code).
  • business leisure controllers that don't require security
  • basic filtering of http addresses (for example, excluding "static" from allowed URLs)

Current Project Levels:
Below is some additional information about my current project structure: Currently I have implemented the following modules:

  • controller: Currently MVC controllers, but I'm going to convert them to REST
  • dto: May change slightly, due to REST approach
  • model: Remains unchanged after talking
  • exception: For business logic
  • repository: will remain unchanged after the conversation
  • service: May change slightly due to micro-services
  • the validator will remain unchanged after the conversation
  • other business logic modules

If I understand correctly, I need to add two additional layers:

  • Config: I have already converted some xml config to java-configs but haven't touched on security configs
  • safety . This is where I will post the managers / authorization / authorization tools. One of the goals of this question is to understand what exactly can be placed here.
  • application class with main method in root package (relative root)
+3


source to share


2 answers


You can start by creating three projects.

  • Auth Server: This will take care of authenticating clients and users by issuing a token, revoking a token, etc.
  • Rest API: all stop controllers, business logic, persistence layer, etc.
  • Front-end: Angular JS, HTML, CSS, etc.

Read about OAuth2 style types.

We use a password type authorization authorization when the server and client authorization is developed by the same organization or when there is a high degree of trust between the resource owner and the client.

The following are the main classes required to implement OAuth2:

A class that extends AuthorizationServerConfigurerAdapter

to configure the authorization server. Here you can set up endpoints such as userDetailsService (a custom class to load user data by username from the database), tokenStore (to store tokens in the database and perform operations on it), clientDetailsService (load client data from the database, your API Rest project can be client).

@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.authenticationManager(authenticationManager);
    endpoints.userDetailsService(userDetailsService);
    endpoints.tokenStore(tokenStore);
    endpoints.setClientDetailsService(clientDetailsService);
    endpoints.accessTokenConverter(accessTokenConverter);
}


@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    //The expression below easies access to /oauth/check_token endpoint from the default denyAll to isAuthenticated.
    oauthServer.checkTokenAccess("isAuthenticated()");
    oauthServer.allowFormAuthenticationForClients();
    oauthServer.passwordEncoder(passwordEncoder);
}


@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientDetailsService);
}

      

A class that continues ResourceServerConfigurerAdapter

. Here you can set up the security configuration for the resource server. The resources would be the stop controllers defined in the Auth servers (for example, controllers to perform a CRUD operation on a user object, an endpoint to revoke a token, controllers to be in Auth Server).

@Override
public void configure(final HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().fullyAuthenticated(); //To restrict all http requests.
   /*http.authorizeRequests().antMatchers("/users/**").permitAll(); //Notice ant matcher here, this tells endpoints which do not require authentication. Lots of http configuration options (like applying filters, cors, csrf etc.) are available here. Please explore*/
}

      

Check out TokenStore default implementation classes (e.g. JdbcTokenStore

, JwtTokenStore

). If you want to use NoSQL db like Cassandra then do custom implementation TokenStore

.

Below is a snippet of sample code for a custom token store used for Cassandra:

@Override
public void storeAccessToken(final OAuth2AccessToken token, final OAuth2Authentication authentication) {

    String refreshToken = null;
    if (token.getRefreshToken() != null) {
        refreshToken = token.getRefreshToken().getValue();
    }

    if (readAccessToken(token.getValue()) != null) {
        removeAccessToken(token.getValue());
    }

    final AccessTokenBuilder accessTokenBuilder = new AccessTokenBuilder();
    accessTokenRepository.save(accessTokenBuilder
                                       .withAuthenticationId(authenticationKeyGenerator.extractKey(authentication))
                                       .withTokenId(extractTokenKey(token.getValue()))
                                       .withTokenBody(ByteBuffer.wrap(serializeAccessToken(token)))
                                       .withUsername(authentication.getName())
                                       .withClientId(authentication.getOAuth2Request().getClientId())
                                       .withAuthentication(ByteBuffer.wrap(serializeAuthentication(authentication)))
                                       .withRefreshTokenId(extractTokenKey(refreshToken))
                                       .build());

}


@Override
public void storeRefreshToken(final OAuth2RefreshToken refreshToken, final OAuth2Authentication authentication) {
    final RefreshTokenBuilder refreshTokenBuilder = new RefreshTokenBuilder();
    refreshTokenRepository.save(refreshTokenBuilder
                                        .withTokenId(extractTokenKey(refreshToken.getValue()))
                                        .withTokenBody(ByteBuffer.wrap(serializeRefreshToken(refreshToken)))
                                        .withAuthentication(ByteBuffer.wrap(serializeAuthentication(authentication)))
                                        .build());
}


@Override
public OAuth2Authentication readAuthentication(final OAuth2AccessToken token) {
    return readAuthentication(token.getValue());
}


@Override
public OAuth2Authentication readAuthentication(final String token) {
    OAuth2Authentication authentication = null;

    try {
        final AccessToken authAccessToken = accessTokenRepository.findByTokenId(extractTokenKey(token));
        authentication = deserializeAuthentication(authAccessToken.getAuthentication().array());
    } catch (final IllegalArgumentException e) {
        removeAccessToken(token);
    }

    return authentication;
}


@Override
public OAuth2AccessToken readAccessToken(final String tokenValue) {
    final AccessToken accessToken = accessTokenRepository.findByTokenId(extractTokenKey(tokenValue));
    return accessToken != null ? deserializeAccessToken(accessToken.getTokenBody().array()) : null;
}

@Override
public OAuth2RefreshToken readRefreshToken(final String tokenValue) {
    final RefreshToken refreshToken = refreshTokenRepository.findOne(extractTokenKey(tokenValue));
    return refreshToken != null ? deserializeRefreshToken(refreshToken.getTokenBody().array()) : null;
}


@Override
public OAuth2Authentication readAuthenticationForRefreshToken(final OAuth2RefreshToken token) {
    return readAuthenticationForRefreshToken(token.getValue());
}


OAuth2Authentication readAuthenticationForRefreshToken(final String tokenValue) {
    final RefreshToken refreshToken = refreshTokenRepository.findOne(extractTokenKey(tokenValue));
    return refreshToken != null ? deserializeAuthentication(refreshToken.getAuthentication().array()) : null;
}


@Override
public OAuth2AccessToken getAccessToken(final OAuth2Authentication authentication) {
    OAuth2AccessToken oAuth2AccessToken = null;
    final String key = authenticationKeyGenerator.extractKey(authentication);
    final AccessToken accessToken = accessTokenRepository.findOne(key);
    if (accessToken != null) {
        oAuth2AccessToken = deserializeAccessToken(accessToken.getTokenBody().array());
        if (oAuth2AccessToken != null && !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(oAuth2AccessToken.getValue())))) {
            removeAccessToken(oAuth2AccessToken.getValue());
            storeAccessToken(oAuth2AccessToken, authentication);
        }
    }

    return oAuth2AccessToken;
}

      



You will need to declare the repository interfaces for db operations. Interfaces that expand CrudRepository

. For most database operations, we do not need to provide an implementation, it is handled by Spring. For a Cassandra implementation, this is a class SimpleCassandraRepository

. Sample code for access token:

public interface AccessTokenRepository extends CrudRepository<AccessToken, String> {

@Query("SELECT * FROM auth_service.oauth_access_token WHERE token_id = :tokenId ALLOW FILTERING")
AccessToken findByTokenId(@Param("tokenId") String tokenId);
}

      

Sample code for ClientDetails

public interface ClientDetailsRepository extends CrudRepository<ClientDetails, String> {

}

      

Please note that we do not need to provide an implementation for these interfaces. Regular CRUD requests are already implemented and honored by Spring.

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {

}

      

Stop API project

Controllers declared here will be called when a request is received from the frontend (AJAX request from javascript). All business logic and resilience level will be here.

This is where you might think about creating a module, a gateway, that talks to Auth Server. This gateway will be between your Rest API and Auth Server. You can use RestTemplate to call a remote Rest service.

If you want no API Rest project to be able to make remote calls to Auth Server, then the client_credentials user also along with password

the grant type. And use OAuth2RestTemplate instead RestTemplate

. Sample code:

<bean id="oAuth2RestTemplate" class="org.springframework.security.oauth2.client.OAuth2RestTemplate">
    <constructor-arg ref="clientCredentialsResourceDetails"/>
    <constructor-arg ref="defaultOAuth2ClientContext"/>
    <property name="requestFactory" ref="httpComponentsClientHttpRequestFactory"/>
</bean>

<bean id="httpComponentsClientHttpRequestFactory" class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
    <constructor-arg ref="selfSignedHttpsClientFactory"/>
</bean>

<bean id="clientCredentialsResourceDetails" class="org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails">
    <property name="accessTokenUri" value="${authentication.service.client.token.url:https://localhost:8443/oauth/token}"/>
    <property name="clientId" value="${authentication.service.client.id:testClient}"/>
    <property name="clientSecret" value="${authentication.service.client.secret:password}"/>
</bean>

<bean id="defaultOAuth2ClientContext" class="org.springframework.security.oauth2.client.DefaultOAuth2ClientContext"/>

      

Hope this was helpful.

+1


source


Not sure if you've seen this, but here's a good article: https://www.toptal.com/java/rest-security-with-jwt-spring-security-and-java , And a github project more or less based on this article: https://github.com/szerhusenBC/jwt-spring-security-demo



+1


source







All Articles