How to Implement Basic Access Authentication in Spring Boot

antozanini

Antonello Zanini

Posted on January 19, 2024

How to Implement Basic Access Authentication in Spring Boot

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 {}
Enter fullscreen mode Exit fullscreen mode

Kotlin

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Auth
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Kotlin

class AuthenticationException(
    message: String = DEFAULT_MESSAGE
) : RuntimeException(message)
Enter fullscreen mode Exit fullscreen mode

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
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Kotlin

@ControllerAdvice
class ControllerExceptionHandler {
    // ...

    @ExceptionHandler(AuthenticationException::class)
    fun forbiddenException(
        e: Exception
    ) : ResponseEntity<String?> {
        return ResponseEntity(
            e.message, 
            HttpStatus.UNAUTHORIZED
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
    }
}
Enter fullscreen mode Exit fullscreen mode

Kotlin

@Configuration
class CustomConfig : WebMvcConfigurationSupport() {
    // ...

    @Bean
    fun authWebArgumentResolverFactory() : HandlerMethodArgumentResolver {
        return BasicAccessAuthenticationResolver()
    }

    override fun addArgumentResolvers(
        argumentResolvers: MutableList<HandlerMethodArgumentResolver>
    ) {
        argumentResolvers.add(authWebArgumentResolverFactory())
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.


The post "How to Implement Basic Access Authentication in Spring Boot" appeared first on Writech.

💖 💪 🙅 🚩
antozanini
Antonello Zanini

Posted on January 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related