Securing Spring Boot REST API with Spring Security Digest Authentication

· Prerequisites
· Overview
  ∘ What is Digest Authentication?
  ∘ Why use Digest Authentication?
  ∘ Basic Authentication vs. Digest Authentication
· Getting Started
  ∘ Creating entities
  ∘ The UserDetailsService
  ∘ Spring Security configuration
  ∘ REST Controller
  ∘ Project structure
· Testing
· Conclusion
· References


Prerequisites

This is the list of all the prerequisites:

  • Spring Boot 3+
  • Maven 3.6.3
  • Java 21
  • Postman / insomnia or any other API testing tool.

Overview

What is Digest Authentication?

Digest Authentication tries to overcome some weaknesses of Basic authentication, specifically by ensuring credentials are never sent in clear text across the wire. The standard governing HTTP Digest Authentication is defined by RFC 2617, which updates an earlier version of the Digest Authentication standard prescribed by RFC 2069.

It works in a challenge/response mode without sending the password over the wire. Because the password is never sent over the wire with the request, TLS isn’t a must. Anyone intercepting the traffic won’t be able to discover the password in cleartext.

Why use Digest Authentication?

Digest authentication provides secure authorization over HTTP because the clear-text password is never transmitted between the client and server. It uses a hash function before transferring the login and password over the network. The use of nonce values in the client challenge also ensures that Digest authentication is resistant to replay attacks.

Basic Authentication vs. Digest Authentication

Cf. Advanced API Security (Securing APIs with OAuth 2.0, OpenID Connect, JWS, and JWE) by Prabath Siriwardena

You should not use Digest Authentication in modern applications, because it is not considered to be secure. The most obvious problem is that you must store your passwords in plaintext or an encrypted or MD5 format. All of these storage formats are considered insecure. Instead, you should store credentials by using a one way adaptive password hash (bCrypt, PBKDF2, SCrypt, and others), which is not supported by Digest Authentication. — https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/digest.html

Getting Started

We will start by creating a simple Spring project from start.spring.io, with the following dependencies: Spring Web, Spring Security, Spring Data JPA, H2 Database, Lombok, and Validation

Creating entities

Let’s define the entities. In the entity package, we’ll create User and Roleclasses.

  • User.java

The user class implements the UserDetails interface that provides security core user information.

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@JsonIgnoreProperties(ignoreUnknown = true)
@ToString
@Entity(name = "app_user")
public class User implements UserDetails {

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
private Long id;

@NotNull
@Size(min = 1, max = 50)
private String username;

@NotBlank
@NotNull
private String password;

private boolean enabled = true;

private boolean accountNonExpired;

private boolean credentialsNonExpired;

private boolean accountNonLocked;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_user", joinColumns = {
@JoinColumn(name = "user_id", referencedColumnName = "id") }, inverseJoinColumns = {
@JoinColumn(name = "role_id", referencedColumnName = "id") })
private List<Role> roles;

@Override
public boolean isEnabled() {
return enabled;
}

@Override
public boolean isAccountNonExpired() {
return !accountNonExpired;
}

@Override
public boolean isCredentialsNonExpired() {
return !credentialsNonExpired;
}

@Override
public boolean isAccountNonLocked() {
return !accountNonLocked;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getCode().name())).collect(Collectors.toSet());
}

}
  • GroupRole.java
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode
@Entity(name = "app_role")
public class Role implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Enumerated(EnumType.STRING)
@Column(length = 15)
private RoleEnum code;
}

The UserDetailsService

DaoAuthenticationProvider uses UserDetailsService to retrieve a username, a password, and other attributes for authenticating with a username and password.

We will need to implement the UserDetailsService interface to override the interface’s loadUserByUsername(String username)method.

public interface UserService extends UserDetailsService {

/**
* @return list of User
*/
List<User> getUsers();

/**
* @param user ussr object
* @return user saved or updated
*/
User save(User user);
}

Let’s define the UserDetailsServiceImpl class as follows:


@Service
@RequiredArgsConstructor
@Transactional
public class UserDetailsServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final AccountStatusUserDetailsChecker detailsChecker = new AccountStatusUserDetailsChecker();

    /**
     * Load user info by credential
     *
     * @param usernameValue username or email
     * @return UserDetails object
     */
    @Override
    public UserDetails loadUserByUsername(String usernameValue) {
        Optional<User> user = userRepository.findActiveByUsername(usernameValue);
        if (user.isEmpty()) {
            throw new UsernameNotFoundException("Invalid username or password.");
        }

        detailsChecker.check(user.get());
        return user.get();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<User> getUsers() {
        return userRepository.findAll();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public User save(User user) {
        return userRepository.save(user);
    }

}

Spring Security configuration

We need to create a new class in a config package called SecurityConfig.java.

SecurityConfig class will have the following configuration:


@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

private final UserDetailsService userDetailsService;

@Bean // (1)
public DigestAuthenticationEntryPoint digestEntryPoint() {
DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
entryPoint.setRealmName("demoDigestAuth");
entryPoint.setKey("571b264a-6868-49e6-9e43-ce80a5749b8f");
return entryPoint;
}

// (2)
public DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter authenticationFilter = new DigestAuthenticationFilter();
authenticationFilter.setUserDetailsService(userDetailsService);
authenticationFilter.setCreateAuthenticatedToken(true);
authenticationFilter.setAuthenticationEntryPoint(digestEntryPoint());
return authenticationFilter;
}


@Bean // (3)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(e -> e.authenticationEntryPoint(digestEntryPoint()))
.addFilterBefore(digestAuthenticationFilter(),DigestAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(toH2Console()).permitAll()
.requestMatchers("/api/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated()
);
return http.build();
}

}
  1. DigestAuthenticationEntryPoint is to send the valid nonce back to the user if authentication fails or to enforce the authentication. It needs a key and a realm name.
  2. DigestAuthenticationFilter processes an HTTP request’s Digest authorization headers, putting the result into the SecurityContextHolder. It requires DigestAuthenticationEntryPoint and UserDetailsService to authenticate the user. If authentication is successful, the resulting Authentication object will be placed into the SecurityContextHolder. If authentication fails, an AuthenticationEntryPoint implementation is called. This must always be DigestAuthenticationEntryPoint, which will prompt the user to authenticate again via Digest authentication.
  3. Defines all filter chains which is capable of being matched against an HttpServletRequest.

REST Controller

Here is an account REST controller that is accessible to connected users and returns the username and role. 

@RestController
@RequestMapping("/api")
public class AccountController {

@GetMapping("/account")
public ResponseEntity<String> getUserInfo() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

var userData = MessageFormat.format("username:{0} - authorities:{1}", authentication.getName() ,authorities);

return ResponseEntity.ok(userData);
}

}

Project structure

This is the final folders & file structure for our project:

Testing

Now we are all done with our code. We can run our application and test it.

First, we initialize the database with some data by loading a data.sql file at startup.

INSERT INTO app_role(id, code) VALUES (1, 'ROLE_ADMIN');
INSERT INTO app_role (id, code) VALUES (2, 'ROLE_GUEST');
INSERT INTO app_role (id, code) VALUES (3, 'ROLE_USER');

INSERT INTO app_user(id, username, password, enabled, account_non_expired, account_non_locked, credentials_non_expired) VALUES (1, 'userdemo', 'jSN&9veq', true, false,false, false);
INSERT INTO app_user(id, username, password, enabled, account_non_expired, account_non_locked, credentials_non_expired) VALUES (2, 'admin', 'B6=]ZHvb', true, false,false, false);

INSERT INTO role_user(role_id, user_id) VALUES (3, 1);
INSERT INTO role_user(role_id, user_id) VALUES (1, 2);
INSERT INTO role_user(role_id, user_id) VALUES (3, 2);

As we see, the user’s password is stored in clear text in the database. This is a major drawback for modern applications.

Authorization header:

Authorization: Digest username="userdemo", realm="demoDigestAuth", 
nonce="MTcyNjc0NzQxNTc1ODplYmE2ZTZmMjAzODU0Mjc2MDgzNmQ0MjQxZDRiMjE3MQ==",
uri="/api/account", algorithm="MD5", qop=auth, nc=00000001,
cnonce="s9oecnte", response="fdea90209f2e9fcc5e584d5e07bf95e2"

Conclusion

Well done !!. In this post, we have seen how to secure a Spring Boot API using Spring Security Digest Authentication.

Although the goal is to replace Basic Authentication, Digest Authentication has many drawbacks. It remedies some, but not all, weaknesses of Basic Authentication.

The complete source code of this series is available on GitHub.

You can reach out to me and follow me on Medium, Twitter, GitHub, Linkedln

Support me through GitHub Sponsors.

Thank you for reading !! See you in the next post.

References

👉 Link to Medium blog

Related Posts