Spring Boot Authentication and Authorization

spring-boot
Published

July 10, 2022

click here to read this in medium

Spring security, JWT, Authorisations

Photo by Florian Berger on Unsplash

🥷 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

Photo by Zeynep Sümer on Unsplash

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:-

Photo by Georg Bommeli on Unsplash

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);
}
}
Photo by Philipp Katzenberger on Unsplash

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);
}
}
Photo by benjamin lehman on Unsplash

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….

Pardhu Guttikonda - Medium