Using a specifically designed framework like Spring Security to achieve your security goals, such as authorization and authentication, may seem the best solution.
“Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.” — Spring Security
However, for basic needs writing a few lines of code to implement your authentication logic might be a better approach. This way, you can keep your application simple and avoid unnecessary dependencies.
The approach I am going to present will allow you to choose to protect an API with basic access authorization. This method allows HTTP user agents to specify a username and password when making requests. The server will authorize the request only if the credentials received are valid. It is called basic because it does not require HTTP cookies, session identifiers, or login pages. In fact, it is the simplest technique for enforcing access controls to web resources, since it is based only on standard fields in the HTTP header.
Let’s see how to achieve basic access authentication in Spring Boot in both Java and Kotlin.
1. Defining a Custom Annotation
To choose what APIs you want to protect by the HTTP basic authentication system, you need a custom-defined annotation. Such an annotation will be used to mark a parameter of type User
to define whether an API requires authentication.
As a consequence, you are assuming that every valid authentication credential pair can be associates with a particular user. So, those credentials will be retrieved from the specific HTTP header and used by authentication logic to extract a user, whose data will be automatically passed to the protected API’s function body.
Let’s see how a custom Auth
annotation can be defined:
Java
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Auth {}
Kotlin
@Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) annotation class Auth
2. Defining Authentication Logic
You should place authentication logic in a specific component, such as BasicAccessAuthenticationHandler
. The purpose of this class is to verify if the credentials received are valid, and extract its related user.
This is what such a class looks like:
Java
@Component public class BasicAccessAuthenticationHandler { // to retrieve users associated to valid credentials private UserDao userDao; public BasicAccessAuthenticationHandler( @Autowired UserDao userDao ) { this.userDao = userDao; } public User getUser( NativeWebRequest nativeWebRequest ) { try { // retrieving credentials the HTTP Authorization Header String authorizationCredentials = nativeWebRequest .getHeader(HttpHeaders.AUTHORIZATION) .substring("Basic".length()) .trim(); // decoding credentials String[] decodedCredentials = new String( Base64 .getDecoder() .decode(authorizationCredentials) ).split(":"); // verifying if the credentials received are valid if ( decodedCredentials[0] == "expectedUsername" && decodedCredentials[1] == "expectedPassword" ) { // user retrieving logic Optional<User> userOptional = userDao.findByUsernameAndPassword(decodedCredentials[0], decodedCredentials[1]); userOptional.orElseThrow( () -> new AuthenticationException() ); return userOptional.get(); } throw new AuthenticationException(); } catch (Exception e) { throw new AuthenticationException(); } } }
Kotlin
@Component class BasicAccessAuthenticationHandler { // to retrieve users associated to valid credentials @Autowired lateinit var UserDao: userDao fun getUser( nativeWebRequest : NativeWebRequest ) : User { try { // retrieving credentials the HTTP Authorization Header val authorizationCredentials = nativeWebRequest .getHeader(HttpHeaders.AUTHORIZATION)!! .substring("Basic".length) .trim() // decoding credentials val decodedCredentials = String( Base64 .getDecoder() .decode(authorizationCredentials) ).split(":") // verifying if the credentials received are valid if ( decodedCredentials[0] == "expectedUsername" && decodedCredentials[1] == "expectedPassword" ) { // user retrieving logic val userOptional = userDao.findByUsernameAndPassword(decodedCredentials[0], decodedCredentials[1]) userOptional.orElseThrow { AuthenticationException() } return userOptional.get() } throw AuthenticationException() } catch (e: Exception) { throw AuthenticationException() } } }
What this component does is extract the credentials from the headers and use them to try to retrieve a user. As defined in the RFC 7617, when dealing with basic access authentication you should expect a header field in the form of Authorization: Basic <credentials>
, where credentials are the Base64 encoding of username and password joined by a single colon :
. This is why you need to decode first and split by :
then.
Plus, as you can see, when credentials are missing or not valid, a custom AuthenticationException
is thrown. In this case, the protected API should respond with the 401 Unauthorized status code.
“The HTTP 401 Unauthorized client error status response code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.” — MDN web docs
Java
public class AuthenticationException extends RuntimeException { public AuthenticationException( String message ) { super(message); } }
Kotlin
class AuthenticationException( message: String = DEFAULT_MESSAGE ) : RuntimeException(message)
To achieve this, a class marked with @ControllerAdvice
can be used as follows:
Java
@ControllerAdvice public class ControllerExceptionHandler { // ... @ExceptionHandler(AuthenticationException.class) public ResponseEntity<String> forbiddenException( Exception e ) { return new ResponseEntity( e.getMessage(), HttpStatus.UNAUTHORIZED ); } }
Kotlin
@ControllerAdvice class ControllerExceptionHandler { // ... @ExceptionHandler(AuthenticationException::class) fun forbiddenException( e: Exception ) : ResponseEntity<String?> { return ResponseEntity( e.message, HttpStatus.UNAUTHORIZED ) } }
3. Enforcing Basic Authentication
To make Spring Boot automatically look for the basic access authentication credentials when the custom Auth
annotation is specified, you need to provide an implementation to theHandlerMethodArgumentResolver
interface.
You can achieve this as follows:
Java
public class BasicAccessAuthenticationResolver implements HandlerMethodArgumentResolver { private BasicAccessAuthenticationHandler basicAccessAuthenticationHandler; public BasicAccessAuthenticationResolver( @Autowired BasicAccessAuthenticationHandler basicAccessAuthenticationHandler ) { this.basicAccessAuthenticationHandler = basicAccessAuthenticationHandler; } // to register the Auth annotation purposely defined @Override public boolean supportsParameter( MethodParameter methodParameter ) { return methodParameter.getParameterAnnotation(Auth.class) != null; } @Override public Object resolveArgument( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory ) { // only if the parameter is of User type if (methodParameter.getParameterType() == User.class) return basicAccessAuthenticationHandler.getUser(nativeWebRequest); // default behavior return UNRESOLVED; } }
Kotlin
class BasicAccessAuthenticationResolver : HandlerMethodArgumentResolver { @Autowired lateinit var basicAccessAuthenticationHandler : BasicAccessAuthenticationHandler // to register the Auth annotation purposely defined override fun supportsParameter( methodParameter: MethodParameter ) : Boolean { return methodParameter.getParameterAnnotation(Auth::class.java) != null } override fun resolveArgument( methodParameter : MethodParameter, modelAndViewContainer : ModelAndViewContainer, nativeWebRequest : NativeWebRequest, webDataBinderFactory : WebDataBinderFactory ) : Any? { // only if the parameter is of User type if (methodParameter.parameterType == User::class.java) return basicAccessAuthenticationHandler.getUser(nativeWebRequest) // default behavior return UNRESOLVED } }
4. Configuring Spring Boot
Now, you only need to define a custom class for the configurations. This way, Spring Boot will be able to use the custom Auth
annotation as designed.
For everything to work, you need to add the previously defined CustomWebResolver
class to the default argument resolvers. This can be achieved by harnessing the WebMvcConfigurationSupport
class.
“[WebMvcConfigurationSupport] is typically imported by adding
@EnableWebMvc
to an application@Configuration
class. An alternative more advanced option is to extend directly from this class and override methods as necessary, remembering to add@Configuration
to the subclass and@Bean
to overridden@Bean
methods.” — Spring’s official documentation
So, you should define a @Configuration
annotated class that extends WebMvcConfigurationSupport
like this:
Java
@Configuration class CustomConfig extends WebMvcConfigurationSupport { // ... @Bean public HandlerMethodArgumentResolver authWebArgumentResolverFactory() { return new BasicAccessAuthenticationResolver(); } @Override public void addArgumentResolvers( List<HandlerMethodArgumentResolver> argumentResolvers ) { argumentResolvers.add(authWebArgumentResolverFactory()) } }
Kotlin
@Configuration class CustomConfig : WebMvcConfigurationSupport() { // ... @Bean fun authWebArgumentResolverFactory() : HandlerMethodArgumentResolver { return BasicAccessAuthenticationResolver() } override fun addArgumentResolvers( argumentResolvers: MutableList<HandlerMethodArgumentResolver> ) { argumentResolvers.add(authWebArgumentResolverFactory()) } }
Please, note that when using WebMvcConfigurationSupport
, you might have to deal with CORS configurations. Otherwise, your APIs may not be reachable as expected.
5. Putting It All Together
Now, it is time to see how Auth
annotation can be used to make an API accessible only to authenticated users. This can be easily achieved by adding a User
type parameter marked with Auth
annotation to the chosen Controller API function:
Java
@GetMapping("data/{id}") public ResponseEntity<Void> getSecuredData( @Auth User user, @PathVariable(value = "id") Int id ) { // NOTE: user can be used inside the method body // API logic return new ResponseEntity(HttpStatus.OK); }
Kotlin
@GetMapping("data/{id}") fun getSecuredData( @Auth user : User, @PathVariable(value = "id") id: Int ) : ResponseEntity<Void> { // NOTE: user can be used inside the method body // API logic return ResponseEntity(HttpStatus.OK) }
Moreover, defining an API lacking protection is possible as well:
Java
@GetMapping("data/{id}") public ResponseEntity<Void> getData( @PathVariable(value = "id") Int id ) { // NOTE: user can be used inside the method body // API logic return new ResponseEntity(HttpStatus.OK); }
Kotlin
@GetMapping("data/{id}") fun getData( @PathVariable(value = "id") id: Int ) : ResponseEntity<Void> { // NOTE: user can be used inside the method body // API logic return ResponseEntity(HttpStatus.OK) }
Conclusion
Neglecting security can have pernicious consequences. This is exactly why you should not let all your APIs be accessible to everyone. By using the basic access authentication method you can protect your most critical APIs. As shown, implementing it and choosing which APIs to protect is not complex, and in most cases, such a simple approach is enough to secure your application.
Thanks for reading! I hope that you found this article helpful.