Security in Spring Boot: UserPasswd, JWT Token, OAuth2

abstract barbed wire black white black and white
Security

Spring Security

Spring Security is a powerful authentication and authorization framework. Provides protection against attacks like session fixation, clickjacking, cross-site request forgery, etc. Spring Security is a highly flexible and customizable framework and is the de-facto standard in the Spring framework.

This blog will cover three options

  • UserName & Password
  • JWT Token
  • OAuth2

Many more Different Authentication Mechanisms are available in Spring Security which is nicely explained in in official docs.

Authentication Architectre

Security

The SecurityContextHolder is where Spring Security stores the details of who is authenticated. Spring Security does not care how the SecurityContextHolder is populated. If it contains a value, then it is used as the currently authenticated user. The simplest way to indicate a user is authenticated is to set SecurityContextHolder directly. SecurityContextHolder uses a ThreadLocal to store these details, which means that the SecurityContext is always available to methods in the same thread.
Spring Security’s FilterChainProxy ensures that the SecurityContext is always cleared.

The Authentication contains:

  • principal – identifies the user. When authenticating with a username/password this is often an instance of UserDetails.
  • credentials – often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked.
  • authorities – the GrantedAuthoritys are high level permissions the user is granted. A few examples are roles or scopes.

AuthenticationManager : is the API that defines how Spring Security’s Filters perform authentication. The Authentication that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’s Filterss) that invoked the AuthenticationManager

Username & Password Login with MySQL DB Storage

Spring Security provides support for username and password is provided through an HTML form.

Step1: Add Dependencies & properties

         <dependency>
			 <groupId>org.springframework.boot</groupId>
			 <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

Declaring the properties will help Spring to Autoconfigure Datasource Bean.

spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity
spring.datasource.username=shoppinguser
spring.datasource.password=shoppingP@ssw0rd
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect

Step2: Create User Entity & Repository

Create User Entity to read Users from DB, Read more on different schemas that Spring supports from docs.

@Entity
@Table(name = "User")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String userName;
    private String password;
    private boolean active;
    private String roles;
   ....... 
   }

Create JPA Repository

public interface UserRepository extends JpaRepository<User, Integer> {
	
    Optional<User> findByUserName(String userName);

}

In the MySQL DB, table user, insert two records of user

insert into user(id, active, password, roles, user_name) values (1,true,"anu", "ROLE_USER", "anu");
insert into user(id, active, password, roles, user_name) values (1,true,"jones", "ROLE_ADMIN", "jones");

Step3: Implement UserDetailsService & UserDetails

The authentication Filter calls AuthenticationManager, and this finally calls loadUserByUsername in our Custom UserDetailsService for retrieving a username, password, and other attributes for authenticating with a username and password. We will be using a JDBC implementation of UserDetailsService here in our example.

MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        Optional<User> user = userRepository.findByUserName(userName);

        user.orElseThrow(() -> new UsernameNotFoundException("Not found: " + userName));

        return user.map(MyUserDetails::new).get();
    }
}

Custom MyUserDetails to Map User

public class MyUserDetails implements UserDetails {

    private String userName;
    private String password;
    private boolean active;
    private List<GrantedAuthority> authorities;

    
    public MyUserDetails(User user) {
        this.userName = user.getUserName();
        this.password = user.getPassword();
        this.active = user.isActive();
        this.authorities = Arrays.stream(user.getRoles().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
    }

.....

Step4: Configure SecurityConfigurion

EnableWebSecurity: This creates Spring security configuration and this creates a Servlet Filter known as the springSecurityFilterChain which is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the login form, etc) within your application.

The configuration in configure(AuthenticationManagerBuilder auth) attempts to obtain the AuthenticationManager for authentication Purposes in MyUserDetailsService, whereas Authorization is being handled at configure(HttpSecurity http)

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers("/user").hasAnyRole("ADMIN", "USER")
                .antMatchers("/").permitAll()
                .and().formLogin();
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

Step5: Run the Application

Add a controller for testing

@RestController
public class HomeController {
	
	@GetMapping("/")
	public String hello() {
		return ("<h1>Welcome</h1>");
	}
	
	@GetMapping("/user")
	public String user() {
		return ("<h1>Welcome User</h1>");
	}
	
	@GetMapping("/admin")
	public String admin() {
		return ("<h1>Welcome admin</h1>");
	}
}

Hit the APP http://localhost:8080/user

GitHub

https://github.com/jonesjalapatgithub/spring-security-demos

JWT token with MySQL

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are: Header, Payload, Signature. Therefore, a JWT typically looks like the following -> xxxxx.yyyyy.zzzzz

Whenever the user wants to access a protected route or resource, the user agent should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following: Authorization: Bearer xxxxx.yyyyy.zzzzz

Step1: Add Dependencies & properties

Below are the JWT specific Dependencies.

		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.3.0</version>
		</dependency>

Please add the JPA, MySQL, security dependencies, and properties similar to the UserPasswd example.

Step2: Create User Entity, Repository, UserDetailsService, & UserDetails

Same as in the UserPasswd example.

Step3: Create JwtUtil with create and get Token methods

@Component
public class JwtUtil implements Serializable {
	
	private static final String SECRET = "changeme";

	public String getUsernameFromToken(String token) {
		return getClaim(token, Claims::getSubject);
	}

	public Date getExpirationDateFromToken(String token) {
		return getClaim(token, Claims::getExpiration);
	}

	public <T> T getClaim(String token, Function<Claims, T> claimsResolver) {
		final Claims claims = getAllClaims(token);
		return claimsResolver.apply(claims);
	}
	
	private Claims getAllClaims(String token) {
		return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
	}

    public String generateToken(UserDetails userDetails) {
		Map<String, Object> claims = new HashMap<>();
		return createToken(claims, userDetails.getUsername());
	}

	private String createToken(Map<String, Object> claims, String subject) {
		return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 60 * 1000))
				.signWith(SignatureAlgorithm.HS512, SECRET).compact();
	}
}

Step4: Create Filter which intercepts the requests and validates token

@Component
public class ShoppingServiceOncePerRequestFilter extends OncePerRequestFilter {

	@Autowired
	private MyUserDetailsService myUserDetailsService;

	@Autowired
	private JwtUtil jwtUtil;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		final String authorizationHeader = request.getHeader("Authorization");
		String username = null;
		String jwt = null;
		if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
			jwt = authorizationHeader.substring(7);
			username = jwtUtil.getUsernameFromToken(jwt);
		}
		if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
			UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
				UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
						userDetails, null, userDetails.getAuthorities());
				usernamePasswordAuthenticationToken
						.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
				SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
		}
	}
}

Step5: Configure SecurityConfiguration

Configure authorization with Session as stateless, and add the previously created filter to intercept all requests and check if they are valid, except for the API /authenticate which we will use to generate an JWT token.

@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private MyUserDetailsService myUserDetailsService;
	
	@Autowired
	ShoppingServiceOncePerRequestFilter shoppingServiceOncePerRequestFilter;


	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(myUserDetailsService);
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()
			.authorizeRequests()
			.antMatchers("/authenticate").permitAll()
			.antMatchers("/admin").hasRole("ADMIN")
            .antMatchers("/user").hasAnyRole("ADMIN", "USER")
			.anyRequest().authenticated()
			.and().sessionManagement()
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.addFilterBefore(shoppingServiceOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return NoOpPasswordEncoder.getInstance();
	}
	
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}

Step6: Running and Testing the App

Add a Authenticate method to create a Auth Token.

@RestController
public class SecurityController {
	
	@Autowired
	private AuthenticationManager authenticationManager;
	
	@Autowired
	private MyUserDetailsService myUserDetailsService;
	
	@Autowired
	private JwtUtil jwtUtil;
	
	//TO DO : handle authentications
	@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
	public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequestModel authenticationRequestModel) {
		authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequestModel.getUsername(),
				authenticationRequestModel.getPassword()));
		final UserDetails userDetails = myUserDetailsService.loadUserByUsername(authenticationRequestModel.getUsername());
		final String jwt = jwtUtil.generateToken(userDetails);
		return ResponseEntity.ok(new AuthenticationResponseModel(jwt) );
	}

	@GetMapping("/user")
	public String user() {
		return ("<h1>Welcome User</h1>");
	}
	
	@GetMapping("/admin")
	public String admin() {
		return ("<h1>Welcome admin</h1>");
	}
}

Invoke the authenticate method HTTP://localhost:8080/authenticate

JWT Token

Invoke the user method HTTP://localhost:8080/user

User Method

GitHub

https://github.com/jonesjalapatgithub/shoppingApp

OAuth2 Based Security

OAuth is a standard that applications can use to provide client applications with “secure delegated access”. It works over HTTP and authorizes Devices, APIs, servers, and applications with access tokens rather than credentials. OAuth also supports authorization workflows as It gives a way to ensure that a specific user has specific permission. However, OAuth doesn’t validate a user’s identity — that’s taken care of by an authentication service like Okta, Google, Facebook, Microsoft, etc.

OAuth2 Validation

OAuth 2.0 has the client request an access token from an authorization server. This access token is then used in the request to the other service for authentication and authorization. The primary benefit here is that the service credentials are only exposed when a new token must be requested or refreshed, also the Token can be saved in client to be used till it Expires.

Step1: Create an Okta account

Create an OpenID Connect App in Okta:  Navigate to Applications and click on Add Application. Select Web and click Next. Give the application a name and specify http://localhost:8080/login as a Login redirect URI & click Done. Save clientId and clientSecret values to be used later.

Step2: Create an Controller Class

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @RestController
    static class SimpleRestController {
    	
        @GetMapping("/hello")
        String sayHello(Principal principal) {
            return "Hello " + (principal != null ? principal.getName() : "anonymous");
        }
        
        @GetMapping("/write")
        String sayHelloPrincipal(Principal principal) {
            return "Hello from writeScope" + (principal != null ? principal.getName() : "anonymous");
        }
    }
}

Step3: Create Resource server configuration

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	http.authorizeRequests()
    	.mvcMatchers(HttpMethod.GET, "/write")
    	.hasAnyAuthority("SCOPE_app_write")
    	.mvcMatchers(HttpMethod.GET, "/hello")
    	.hasAnyAuthority("SCOPE_app_read")
        .anyRequest().denyAll()
        .and()
        .oauth2ResourceServer()
        .jwt();
    }
}

Step4 : Configure properties and Run the Resource Server

okta.oauth2.issuer=ObtainfromStep1
okta.oauth2.client-id=ObtainfromStep1
okta.oauth2.client-secret=ObtainfromStep1
spring.jpa.defer-datasource-initialization=true

Step5 : Invoke the API using Spring Boot Client APP

Dependencies

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

OAuthClientConfiguration

@Configuration
public class OAuthClientConfiguration {

    // Create the Okta client registration
    @Bean
    ClientRegistration oktaClientRegistration(
            @Value("${spring.security.oauth2.client.provider.okta.token-uri}") String token_uri,
            @Value("${spring.security.oauth2.client.registration.okta.client-id}") String client_id,
            @Value("${spring.security.oauth2.client.registration.okta.client-secret}") String client_secret,
            @Value("${spring.security.oauth2.client.registration.okta.scope}") String scope,
            @Value("${spring.security.oauth2.client.registration.okta.authorization-grant-type}") String authorizationGrantType
    ) {
        return ClientRegistration
                .withRegistrationId("okta")
                .tokenUri(token_uri)
                .clientId(client_id)
                .clientSecret(client_secret)
                .scope(scope)
                .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
                .build();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository(ClientRegistration oktaClientRegistration) {
        return new InMemoryClientRegistrationRepository(oktaClientRegistration);
    }

    @Bean
    public OAuth2AuthorizedClientService auth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    @Bean
    public AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientServiceAndManager (
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService) {

        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials()
                        .build();

        AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
}

Application class

@Configuration
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

	Logger logger = LoggerFactory.getLogger(CommandLineRunner.class);

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Autowired
	private AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientServiceAndManager;

	@Override
	public void run(String... args) throws Exception {
		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
				.principal("Demo Service")
				.build();
		OAuth2AuthorizedClient authorizedClient = this.authorizedClientServiceAndManager.authorize(authorizeRequest);
		OAuth2AccessToken accessToken = Objects.requireNonNull(authorizedClient).getAccessToken();

		logger.info("Issued: " + accessToken.getIssuedAt().toString() + ", Expires:" + accessToken.getExpiresAt().toString());
		logger.info("Scopes: " + accessToken.getScopes().toString());
		logger.info("Token: " + accessToken.getTokenValue());

		HttpHeaders headers = new HttpHeaders();
		headers.add("Authorization", "Bearer " + accessToken.getTokenValue());
        HttpEntity request = new HttpEntity(headers);

		RestTemplate restTemplate = new RestTemplate();
		ResponseEntity<String> response = restTemplate.exchange(
				"http://localhost:8080/write",
				HttpMethod.GET,
				request,
				String.class
		);

		String result = response.getBody();
		logger.info("Reply = " + result);
	}
}

Properties

spring.security.oauth2.client.registration.okta.client-id=ObtainfromStep1
spring.security.oauth2.client.registration.okta.client-secret=ObtainfromStep1
spring.security.oauth2.client.registration.okta.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.okta.scope=app_write
spring.security.oauth2.client.provider.okta.token-uri=https://dev-7****4.okta.com/oauth2/default/v1/token
spring.main.web-application-type=none
server.port=8081

Output

HMAC(Hash based Message Authentication Code)

HMAC (Hash-based Message Authentication Code) is a type of message authentication code (MAC) that is acquired by executing a cryptographic hash function on the data and a secret shared key. The cryptographic hash function may be MD-5, SHA-1, or SHA-256. When the client requests the server, it adds this hash to the Authorization header within the request. When the server receives the request, it makes its own HMAC. Both the HMACS are compared and if both are equal, the client is considered legitimate. We can add Date time in header to add expiry to signature.

HMAC_SHA256(“key”, “The quick brown fox jumps over the lazy dog”) = f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

    	public static String generateHMAC() {
    	    try {
    	    	String key = "changeme";
    	    	String data = "data";
    	        Mac hmac = Mac.getInstance("HmacSHA256");
    	        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
    	        hmac.init(secret_key);
    	        return new String(Hex.encodeHex(hmac.doFinal(data.getBytes("UTF-8"))));
    	    } catch (Exception e) {
    	        throw new RuntimeException(e);
    	    }
    	}

References

3 thoughts on “Security in Spring Boot: UserPasswd, JWT Token, OAuth2”

  1. Oh my goodness! Incredible article dude! Thank you, However I am encountering difficulties with your RSS. I don’t understand the reason why I am unable to join it. Is there anybody else having identical RSS problems? Anybody who knows the answer can you kindly respond? Thanx!!

Leave a Comment

Your email address will not be published.

error: Content is protected !!
Scroll to Top