Spring Boot Authentication and Authorization
Spring security, JWT, Authorisations
🥷 What we do
In this article, We will be creating a Spring boot application to demonstrate Authentication and Authorization to users. For this Demo, we will be using MongoDB database. Also For this Authentication we will be using JWT Standard, and we will be using HS512 algorithm to encode the information.
🎯Dependencies used :
please view at link . (https://github.com/propardhu/AuthDemoSpringBoot/blob/main/build.gradle)
🧞♂️Steps To be followed
- Create domain and repository for Both users and Authorities.
- Prepare JWT and Security Utilities.
- Configure spring security and JWT Filters.
- mongock ChangeLog to add initial users to DataBase.
Document Structures of Both User and Authorities
Authority.java
/**
* An authority (a security role) used by Spring Security.
*/
@Document(collection = "authority")
public class Authority implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull
@Size(max = 50)
@Id
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Authority)) {
return false;
}
return Objects.equals(name, ((Authority) o).name);
}
@Override
public int hashCode() {
return Objects.hashCode(name);
}
// prettier-ignore
@Override
public String toString() {
return "Authority{" +
"name='" + name + '\'' +
"}";
}
}
User.java
/**
* A user.
*/
@org.springframework.data.mongodb.core.mapping.Document(collection = "user")
public class User extends AbstractAuditingEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
@NotNull
@Pattern(regexp = Constants.LOGIN_REGEX)
@Size(min = 1, max = 50)
@Indexed
private String login;
@JsonIgnore
@NotNull
@Size(min = 60, max = 60)
private String password;
@Size(max = 50)
@Field("first_name")
private String firstName;
@Size(max = 50)
@Field("last_name")
private String lastName;
@Email
@Size(min = 5, max = 254)
@Indexed
private String email;
private boolean activated = false;
@Size(min = 2, max = 10)
@Field("lang_key")
private String langKey;
@Size(max = 256)
@Field("image_url")
private String imageUrl;
@Size(max = 20)
@Field("activation_key")
@JsonIgnore
private String activationKey;
@Size(max = 20)
@Field("reset_key")
@JsonIgnore
private String resetKey;
@Field("reset_date")
private Instant resetDate = null;
@JsonIgnore
private Set<Authority> authorities = new HashSet<>();
}
UserRepository.java
@Repository
public interface UserRepository extends MongoRepository<User, String> {
Optional<User> findOneByActivationKey(String activationKey);
List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime);
Optional<User> findOneByResetKey(String resetKey);
Optional<User> findOneByEmailIgnoreCase(String email);
Optional<User> findOneByLogin(String login);
Page<User> findAllByIdNotNullAndActivatedIsTrue(Pageable pageable);
}
AuthorityRepository.java
/**
* Spring Data MongoDB repository for the {@link Authority} entity.
*/
public interface AuthorityRepository extends MongoRepository<Authority, String> {}
AuthoritiesConstants.java
public final class AuthoritiesConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
public static final String ANONYMOUS = "ROLE_ANONYMOUS";
private AuthoritiesConstants() {}
}
JWT Things
👉 JWT working Flow:-
- JSON Web Token(JWT) is an open standard used to share security information between two parties like client and server. It follows one particular cryptographic algorithm to encrypt and decrypt the JSON Objects. Algorithms like Hash 512,Hash 256, RS256 etc.
- When a user registers in an application, user details are sent to the server. While saving the user details. We will ensure to encrypt the password while saving into the database.(BCryptPasswordEncoder)
- When a user logs in to the application, details like username and password will be sent to the server. There we will be verifying the password with encrypted password. If matches, we will be creating an JWT token and sent it as a response.
- After getting the JWT token, we need to append the token in the header of HTTP request (For all secured endpoints, we need to follow the same).
- The JWT token contains three parts (HEADER, PAYLOAD)are Base64-URL encoded JSON and Cryptographic Signature.
Note:- We need a secret key to encrypt and decrypt data. - For Authorization, we will be adding the roles of the user to the token itself.
TokenProvider.java
Here we will be writing the methods to createTokens, getAuthentications from token and validate token.
Replace “KEY” with secretKey.
@Component
public class TokenProvider {
private final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private final Key key;
private final JwtParser jwtParser;
private final long tokenValidityInMilliseconds;
private final long tokenValidityInMillisecondsForRememberMe;
public TokenProvider() {
byte[] keyBytes;
String secret = "KEY";
if (!ObjectUtils.isEmpty(secret)) {
log.debug("Using a Base64-encoded JWT secret key");
keyBytes = Decoders.BASE64.decode(secret);
} else {
log.warn(
"Warning: the JWT key used is not Base64-encoded. " +
"We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security."
);
secret = "YWQzMmJiZjgwMDliY2M4NWE0ZjVkOWUxZmRjYTcwMDc2OTZkN2Y5MzQ3ODQ4N2M2YmExNTVmNDFjMDdhZGUzZDRmZDE2OGFkMTc1NmE4MWVmYTIxZDI3YWIzZTNhNzQ1YjNhMzE1ZGVmMWRhNWQxZGFhN2I3NjQzMWRkNjczODY=";
keyBytes = secret.getBytes(StandardCharsets.UTF_8);
}
key = Keys.hmacShaKeyFor(keyBytes);
jwtParser = Jwts.parserBuilder().setSigningKey(key).build();
this.tokenValidityInMilliseconds = 1000 * 700;
this.tokenValidityInMillisecondsForRememberMe = 1000 * 700;
}
public String createToken(Authentication authentication, boolean rememberMe) {
String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity;
if (rememberMe) {
validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
} else {
validity = new Date(now + this.tokenValidityInMilliseconds);
}
return Jwts
.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
Collection<? extends GrantedAuthority> authorities = Arrays
.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.filter(auth -> !auth.trim().isEmpty())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public boolean validateToken(String authToken) {
try {
jwtParser.parseClaimsJws(authToken);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.info("Invalid JWT token.");
log.trace("Invalid JWT token trace.", e);
}
return false;
}
}
JWTConfigurer.java
Now We need to add JWTFilter with tokenProvider to the HttpSecurity, That can be overwritten by extending SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>.
public class JWTConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
public JWTConfigurer(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void configure(HttpSecurity http) {
JWTFilter customFilter = new JWTFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JWTFilter.java
This class will be extending the GenericFilterBean class and we can add Filter by overriding doFilter method. So here we will be validating all the request with bearer token. if the request do not have it can access only public API’s.
public class JWTFilter extends GenericFilterBean {
public static final String AUTHORIZATION_HEADER = "Authorization";
private final TokenProvider tokenProvider;
public JWTFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Service which provides user details from DataBase need to be declared as component to avoid dependency cycle in our project.
/**
* Authenticate a user from the database.
*/
@Component("userDetailsService")
public class DomainUserDetailsService implements UserDetailsService {
private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class);
private final UserRepository userRepository;
public DomainUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(final String login) {
log.debug("Authenticating {}", login);
if (new EmailValidator().isValid(login, null)) {
return userRepository
.findOneByEmailIgnoreCase(login)
.map(user -> createSpringSecurityUser(login, user))
.orElseThrow(() -> new UsernameNotFoundException("User with email " + login + " was not found in the database"));
}
String lowercaseLogin = login.toLowerCase(Locale.ENGLISH);
return userRepository
.findOneByLogin(lowercaseLogin)
.map(user -> createSpringSecurityUser(lowercaseLogin, user))
.orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database"));
}
private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) {
if (!user.isActivated()) {
throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
}
List<GrantedAuthority> grantedAuthorities = user
.getAuthorities()
.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), grantedAuthorities);
}
}
SecurityUtils.java
Here we will be writing methods related to current user login like getCurrentUserName etc.
/**
* Utility class for Spring Security.
*/
public final class SecurityUtils {
private SecurityUtils() {}
/**
* Get the login of the current user.
*
* @return the login of the current user.
*/
public static Optional<String> getCurrentUserLogin() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication()));
}
private static String extractPrincipal(Authentication authentication) {
if (authentication == null) {
return null;
} else if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
return springSecurityUser.getUsername();
} else if (authentication.getPrincipal() instanceof String) {
return (String) authentication.getPrincipal();
}
return null;
}
/**
* Get the JWT of the current user.
*
* @return the JWT of the current user.
*/
public static Optional<String> getCurrentUserJWT() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return Optional
.ofNullable(securityContext.getAuthentication())
.filter(authentication -> authentication.getCredentials() instanceof String)
.map(authentication -> (String) authentication.getCredentials());
}
/**
* Check if a user is authenticated.
*
* @return true if the user is authenticated, false otherwise.
*/
public static boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && getAuthorities(authentication).noneMatch(AuthoritiesConstants.ANONYMOUS::equals);
}
/**
* Checks if the current user has any of the authorities.
*
* @param authorities the authorities to check.
* @return true if the current user has any of the authorities, false otherwise.
*/
public static boolean hasCurrentUserAnyOfAuthorities(String... authorities) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (
authentication != null && getAuthorities(authentication).anyMatch(authority -> Arrays.asList(authorities).contains(authority))
);
}
/**
* Checks if the current user has none of the authorities.
*
* @param authorities the authorities to check.
* @return true if the current user has none of the authorities, false otherwise.
*/
public static boolean hasCurrentUserNoneOfAuthorities(String... authorities) {
return !hasCurrentUserAnyOfAuthorities(authorities);
}
/**
* Checks if the current user has a specific authority.
*
* @param authority the authority to check.
* @return true if the current user has the authority, false otherwise.
*/
public static boolean hasCurrentUserThisAuthority(String authority) {
return hasCurrentUserAnyOfAuthorities(authority);
}
private static Stream<String> getAuthorities(Authentication authentication) {
return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority);
}
}
Security Configurations
SecurityConfiguration.java
Here we will be saying what kind of api’s need to be permitted as public api’s.
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(
TokenProvider tokenProvider,
CorsFilter corsFilter,
SecurityProblemSupport problemSupport
) {
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**").antMatchers("/swagger-ui/**").antMatchers("/test/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.csrf()
.disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.headers()
.contentSecurityPolicy("default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:")
.and()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.permissionsPolicy().policy("camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()")
.and()
.frameOptions()
.deny()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/health/**").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.httpBasic()
.and()
.apply(securityConfigurerAdapter());
// @formatter:on
}
private JWTConfigurer securityConfigurerAdapter() {
return new JWTConfigurer(tokenProvider);
}
}
WebConfigure.java
Allowed domains needs be to added here to avoid COR’s related issues
/**
* Configuration of web application with Servlet 3.0 APIs.
*/
@Configuration
public class WebConfigurer implements ServletContextInitializer {
private final Logger log = LoggerFactory.getLogger(WebConfigurer.class);
private final Environment env;
public WebConfigurer(Environment env) {
this.env = env;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
if (env.getActiveProfiles().length != 0) {
log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles());
}
log.info("Web application fully configured");
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
List<String> list = new ArrayList<>();
list.add("*");
config.setAllowedOriginPatterns(list);
if (!CollectionUtils.isEmpty(config.getAllowedOrigins()) || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) {
log.debug("Registering CORS filter");
source.registerCorsConfiguration("/api/**", config);
source.registerCorsConfiguration("/management/**", config);
source.registerCorsConfiguration("/v2/api-docs", config);
source.registerCorsConfiguration("/v3/api-docs", config);
source.registerCorsConfiguration("/swagger-resources", config);
source.registerCorsConfiguration("/swagger-ui/**", config);
}
return new CorsFilter(source);
}
}
Mongock ChangeLog
We will be using mongock to add default user details to database like admin.
In application.properties we need to give the path of the class which contains ChangeLog annotation.
spring.data.mongodb.uri=mongodb://localhost:27017/AuthDemo
mongock.change-logs-scan-package=
com.pardhu.authdemo.config.InitialSetupMigration
InitialSetupMigration.java
/**
* Creates the initial database setup.
*/
@ChangeLog(order = "001")
public class InitialSetupMigration {
@ChangeSet(order = "01", author = "initiator", id = "01-addAuthorities")
public void addAuthorities(MongockTemplate mongoTemplate) {
Authority adminAuthority = new Authority();
adminAuthority.setName(AuthoritiesConstants.ADMIN);
Authority userAuthority = new Authority();
userAuthority.setName(AuthoritiesConstants.USER);
mongoTemplate.save(adminAuthority);
mongoTemplate.save(userAuthority);
}
@ChangeSet(order = "02", author = "initiator", id = "02-addUsers")
public void addUsers(MongockTemplate mongoTemplate) {
Authority adminAuthority = new Authority();
adminAuthority.setName(AuthoritiesConstants.ADMIN);
Authority userAuthority = new Authority();
userAuthority.setName(AuthoritiesConstants.USER);
User adminUser = new User();
adminUser.setId("user-1");
adminUser.setLogin("admin");
adminUser.setPassword("$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC");
adminUser.setFirstName("admin");
adminUser.setLastName("Administrator");
adminUser.setEmail("admin@localhost");
adminUser.setActivated(true);
adminUser.setLangKey("en");
adminUser.setCreatedBy(Constants.SYSTEM);
adminUser.setCreatedDate(Instant.now());
adminUser.getAuthorities().add(adminAuthority);
adminUser.getAuthorities().add(userAuthority);
mongoTemplate.save(adminUser);
User userUser = new User();
userUser.setId("user-2");
userUser.setLogin("user");
userUser.setPassword("$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K");
userUser.setFirstName("");
userUser.setLastName("User");
userUser.setEmail("user@localhost");
userUser.setActivated(true);
userUser.setLangKey("en");
userUser.setCreatedBy(Constants.SYSTEM);
userUser.setCreatedDate(Instant.now());
userUser.getAuthorities().add(userAuthority);
mongoTemplate.save(userUser);
}
}
Now we have two Authorities. Admin and User.
While writing an api in controller itself we can annotate like this api can be accessed by admin only using annotations →
@PreAuthorize(“hasAuthority(\”” + AuthoritiesConstants.ADMIN + “\”)”)
@PostAuthorize(“hasAuthority(\”” + AuthoritiesConstants.ADMIN + “\”)”)
Complete working git repo is available at GitHub . Also we wrote few login and register API’s.
Thank you….