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.
- Username and Password – how to authenticate with a username/password
- OAuth 2.0 Login – OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub)
- SAML 2.0 Login – SAML 2.0 Log In
- Central Authentication Server (CAS) – Central Authentication Server (CAS) Support
- Remember Me – how to remember a user past session expiration
- JAAS Authentication – authenticate with JAAS
- OpenID – OpenID Authentication (not to be confused with OpenID Connect)
- Pre-Authentication Scenarios – authenticate with an external mechanism such as SiteMinder or Java EE security but still use Spring Security for authorization and protection against common exploits.
- X509 Authentication – X509 Authentication
- Spring Security
- Authentication Architectre
- Username & Password Login with MySQL DB Storage
- Welcome
- Welcome User
- Welcome admin
- GitHub
- JWT token with MySQL
- Step1: Add Dependencies & properties
- Step2: Create User Entity, Repository, UserDetailsService, & UserDetails
- Step3: Create JwtUtil with create and get Token methods
- Step4: Create Filter which intercepts the requests and validates token
- Step5: Configure SecurityConfiguration
- Step6: Running and Testing the App
- Welcome User
- Welcome admin
Authentication Architectre
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 ofUserDetails
.credentials
– often a password. In many cases this will be cleared after the user is authenticated to ensure it is not leaked.authorities
– theGrantedAuthority
s 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 Filters
s) 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
Invoke the user method HTTP://localhost:8080/user
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.
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
- https://www.youtube.com/watch?v=LKvrFltAgCQ&list=PLqq-6Pq4lTTYTEooakHchTGglSvkZAjnE&index=7
- https://www.youtube.com/watch?v=X80nJ5T7YpE&list=PLqq-6Pq4lTTYTEooakHchTGglSvkZAjnE&index=15
- https://docs.spring.io/spring-security/reference/servlet/getting-started.html
- https://jwt.io/introduction
- https://developer.okta.com/blog/2020/11/24/spring-boot-okta
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!!
I have disabled RSS feeds for now on the website, probably will enable it in future.
I was pretty pleased to discover this great site. I need to to thank you for your time for this particularly fantastic read!! I definitely appreciated every part of it and I have you book marked to see new stuff on your blog.