Advent Calendar 2025 – Basic Login Solution – Part 1
Introduction
The administration interface of a URL shortener is a sensitive area where short links can be changed, removed, or assigned expiration dates. Although the system is often operated on an internal server or in a private environment, protecting this management interface remains a fundamental security concern. Accidental access by unauthorised users can not only lead to incorrect forwarding or data loss, but also undermine trust in the overall system.
With the introduction of a configurable login mechanism, precise access control is introduced, deliberately separating access to management functions from the rest of the system. The login serves as a lightweight security measure that does not require external dependencies, frameworks or time-consuming user management. It is precisely this simplicity – a single password, a simple configuration file and a centred login screen – that makes the solution particularly attractive for small deployments or personal projects.
The source code for this article can be found on GitHub at the following URL: https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-09



Objectives of the implementation
The introduction of a simple, configurable login mechanism aims to achieve several clearly defined goals that improve both the security and usability of the URL shortener. The focus is not on establishing a complex user and role model, but on creating a pragmatic protective layer that reliably controls administrative access without unnecessarily increasing development or operating costs.
First, the primary objective is to secure administrative functions. The management interface allows you to create, edit, and remove short links, as well as set or clean expiration dates. These functions must be protected against unauthorised access, particularly if the URL shortener is not operated exclusively within the closed network. A simple login prevents accidental or curious access from causing damage.
Another goal is to minimise the barrier to entry. The solution should work without additional frameworks, external identity providers, or complex configuration. The implementation is deliberately based on the project’s philosophy: Everything remains manageable, lightweight, and understandable. With a simple auth.properties file, the system can be turned on or off at will, and a single password suffices for access control.
In addition, the login mechanism is designed to ensure a predictable and consistent user experience. These include a well-designed login page, an immediate redirect on unauthenticated access to protected areas, and a transparent logout process that cleanly ends the session. All these aspects ensure that the login fits naturally into the existing UI and is still clearly perceived as a security measure.
Finally, the login also serves as a foundation for potential future expansions. Although the current implementation is deliberately minimalist, it provides the structural prerequisites to respond to stronger security requirements if necessary—for example, by adding hashing mechanisms, time-limited session tokens, or server-side password rotation. This leaves the system open for further development without losing its current simplicity.
Configurable login via auth.properties
The new login mechanism is based on a deliberately simple configuration file that enables centralised control of the application’s authentication behaviour. This file is named auth.properties and is located in the application’s resource directory. By using them, the login system can not only be conveniently activated or deactivated, but also quickly adapted to different deployment scenarios.

The focus is on two configuration keys: login.enabled and login.password. While the first parameter checks whether the login system is active, the second parameter specifies the required access password. These values are automatically read when the application starts and henceforth determine the behaviour of all protected areas. It is precisely this mechanism that enables turning off login on short notice or changing the password without modifying the source code again.
The configuration is read in by the LoginConfigInitializer, which is executed when the servlet container starts. It checks whether the file exists, loads its contents and passes it to the central class LoginConfig, which manages these values and makes them available for later access.
This class is responsible for correctly configuring the application’s login behaviour at startup. This ensures that both the login page and the route protection can access a uniform configuration basis at all times.
After loading the file, the LoginConfig class assumes responsibility for persistently storing the values and performing password comparisons.
This form of configuration control not only simplifies operation but also supports different operating modes. For example, a fully disabled login system is suitable for purely internal development environments, while the activated mode protects against unauthorised changes in production or publicly available environments. Switching between the two modes is kept as low-threshold as possible – a simple value in a text file is enough.
Although this mechanism provides basic protection, it is not designed to be a highly secure authentication solution. Instead, the goal is to establish a minimum level of security to protect the administrative area from accidental access or unauthorised use. For productive environments with increased security requirements, additional measures such as password hashing, multi-factor authentication, or integration into established identity services would be necessary.
LoginConfig & Initialization
Central control of login behaviour is based on two closely interlinked components: the LoginConfig class itself and its upstream initialiser, LoginConfigInitializer. Together, they ensure that the configuration from the auth.properties file is correctly read, interpreted, and made available to the application runtime.
The first focus is on the LoginConfig class. It provides a minimalist yet coherent foundation for the login system. The approach is intentionally simple: there is no user base, roles, or profiles, just a single password that serves as an access threshold. The class manages this password and the information on whether the login should be active. The structure remains manageable to keep the entry barrier for administrators and developers low.
An essential detail is the division into two phases: first, when the application starts, the LoginConfigInitializer checks which settings are stored in the configuration file. These values are then passed to the static LoginConfig.initialise() method, which populates the appropriate fields and makes them available to the rest of the application.
This initialisation process occurs entirely when the servlet container starts. This ensures that all views loaded later, especially the route protection and the login page, have access to a consistent and fully initialised configuration. This avoids error conditions that could arise from missing or delayed loading of configuration values.
In the following, we will take a closer look at both components and refer to the source code for clarity.
LoginConfig – central configuration class
The core is the LoginConfig class, which stores the login switch and the expected password as byte data. It is declared final and has a private constructor, so it is used exclusively through its static methods:
/**
* Central configuration for the simple admin login.
* Reads its values from auth.properties via LoginConfigInitializer.
*/
public final class LoginConfig {
private static volatile boolean loginEnabled;
private static volatile byte[] expectedPasswordBytes;
private LoginConfig() {
}
/**
* Initialises the login configuration.
*
* @param enabled whether the login mechanism should be enforced
* @param password the raw password read from configuration, may be {@code null}
*/
public static void initialise(boolean enabled, String password) {
loginEnabled = enabled;
if (!enabled || password == null || password.isBlank()) {
expectedPasswordBytes = null;
return;
}
expectedPasswordBytes = password.getBytes(StandardCharsets.UTF_8);
}
/**
* @return {@code true} if login protection is enabled at all
*/
public static boolean isLoginEnabled() {
return loginEnabled;
}
/**
* @return {@code true} if login is enabled and a usable password has been configured
*/
public static boolean isLoginConfigured() {
return loginEnabled
&&expectedPasswordBytes != null
&& expectedPasswordBytes.length > 0;
}
/**
* Compares the entered password with the configured one using constant-time comparison.
*/
public static boolean matches(char[] enteredPassword) {
if (!isLoginConfigured() || enteredPassword == null) {
return false;
}
byte[] entered = new String(enteredPassword).getBytes(StandardCharsets.UTF_8);
boolean result = MessageDigest.isEqual(expectedPasswordBytes, entered);
Best-effort clean-up
Arrays.fill(entered, (byte) 0);
return result;
}
}
The initialise method is called only when the initialiser starts and determines whether login is enabled (loginEnabled) and whether a valid password is present. Invalid or empty passwords are consistently discarded; isLoginConfigured() only returns true if there is a usable configuration.
For the actual password comparison, matches(char[] enteredPassword) serves as the central function. It takes the entered password as a char[], converts it to a byte array in UTF-8 format, and compares it with the expected byte sequence using MessageDigest.isEqual. This enables constant-time comparison, making simple timing attacks more difficult. The temporary byte array is then overwritten using Arrays.fill to remove the sensitive data from memory, at least as effectively as possible.
LoginConfigInitializer – Load configuration on startup
For LoginConfig to work with meaningful values, the configuration file must be read when the servlet container starts. This task is performed by the LoginConfigInitializer class, which is registered as @WebListener and implements the ServletContextListener interface :
@WebListener
public class LoginConfigInitializer implements ServletContextListener, HasLogger {
private static final String PROPERTIES_PATH = "auth.properties";
@Override
public void contextInitialized(ServletContextEvent sce) {
logger().info("Initialising LoginConfig from {}", PROPERTIES_PATH);
Properties props = new Properties();
try (InputStream in = getClass()
.getClassLoader()
.getResourceAsStream(PROPERTIES_PATH)) {
if (in == null) {
logger().warn("No {} found on classpath. Login will be disabled.", PROPERTIES_PATH);
LoginConfig.initialise(false, null);
return;
}
props.load(in);
String enabledRaw = props.getProperty("login.enabled", "true").trim();
boolean enabled = Boolean.parseBoolean(enabledRaw);
String password = props.getProperty("login.password");
LoginConfig.initialise(enabled, password);
if (!enabled) {
logger().info("Login explicitly disabled via login.enabled=false");
} else if (LoginConfig.isLoginConfigured()) {
logger().info("LoginConfig initialised successfully from {}", PROPERTIES_PATH);
} else {
logger().warn("login.enabled=true but no usable password configured. "
+ "Login will effectively be disabled.");
}
} catch (IOException e) {
logger().error("Failed to load " + PROPERTIES_PATH + ". Login will be disabled.", e);
LoginConfig.initialise(false, null);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
Nothing to clean up
}
}
The initialiser follows a straightforward process: first, it attempts to load the auth.properties file from the classpath. If this is not possible, the login is explicitly deactivated, and a warning log is written. If successful, the configuration values “login.enabled" and “login.password" are read, converted to appropriate data types, and forwarded to LoginConfig.initialise. Finally, depending on the configuration, additional log output is generated, providing quick information about the active mode during operation.
This separation of responsibilities creates a well-traceable initialisation path: LoginConfigInitializer handles loading and interpreting the configuration file. At the same time, LoginConfig encapsulates the actual login logic and is later used by other components, such as the login view or route protection.
The new login page
With the introduction of admin login, the application gains its own entry page that deliberately distinguishes itself from the rest of the UI. The new login page serves as the boundary between public functions, such as link forwarding, and sensitive administrative functions in the backend. The goal was to create a reduced, highly focused layout that integrates seamlessly with the application’s visual design while remaining clearly recognisable as a security barrier.

Instead of being embedded in the existing navigation layout, the login is rendered as a standalone view without MainLayout. Users will not see any page navigation, drawer or admin functions until they have successfully authenticated. The page fills the entire browser window; its contents are centred vertically and horizontally. This creates the impression of a classic login screen with a single task: requesting the password for the administrative area.
At the centre is a compact login panel comprising a headline, a short explanatory text, a password field, and a login button. The headline clearly conveys that this is admin access, while the accompanying text explains why a password is required and what area it protects. This improves transparency and reduces queries, especially when multiple users use the system.
The password field is configured to automatically receive focus when the page is loaded. Users can therefore type directly without first clicking with the mouse. In addition, a clear button lets you remove an accidentally entered string with a single click. Displaying the password in plain text is deliberately avoided to reduce the risk of shoulder-surfing, especially in shared work environments.
The input behaviour is also designed to ensure a smooth process. Authentication can be done either by clicking on the login button or by pressing Enter. If the login fails, the view marks the password field as invalid and displays a clear error message indicating that the entered password is incorrect. This preserves context and allows the user to start a second attempt immediately without reloading the page.
Implementation of the LoginView
The technical implementation of the described login page is handled by the LoginView class. It is registered as a separate route and deliberately dispenses with a layout to enable a focused, full-surface login screen:
@Route(LoginView.PATH) // No layout = no navigation or drawer visible
@PageTitle("Admin Login | URL Shortener")
public class LoginView
extends VerticalLayout
implements BeforeEnterObserver {
public static final String PATH = "login";
private final PasswordField passwordField = new PasswordField("Password");
private final Button loginButton = new Button("Login");
public LoginView() {
setSizeFull();
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
configureForm();
buildLayout();
}
It is already evident from the declaration that LoginView is not embedded in MainLayout. Because the route annotation lacks a layout, the page is displayed in isolation; the layout properties in the constructor handle complete vertical and horizontal centring. The two central UI components – password field and login button – are kept as fields so that they can be further configured in the helper methods configureForm() and buildLayout().
The configuration of the form is deliberately designed for a lean but fluid user experience:
private void configureForm() {
passwordField.setAutofocus(true);
passwordField.setWidth("300px");
passwordField.setClearButtonVisible(true);
passwordField.setRevealButtonVisible(false);
passwordField.setInvalid(false);
loginButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
loginButton.setWidth("300px");
loginButton.addClickListener(_ -> attemptLogin());
passwordField.addKeyDownListener(event -> {
if ("Enter".equalsIgnoreCase(event.getKey().getKeys().getFirst())) {
attemptLogin();
}
});
passwordField.addValueChangeListener(_ -> passwordField.setInvalid(false));
}
The password field automatically gains focus when the page loads and is set to a fixed width, so that the input and button visually form a single unit. The Clear button lets you quickly delete an incorrect entry; the password is displayed in plain text by default. The login button is marked as the primary button and, when pressed, triggers the attemptLogin() method. The value change listener ensures that a previously set error state is restored when typing again.
The structure of the panel that appears in the middle of the screen is defined in buildLayout():
private void buildLayout() {
H2 title = new H2("Admin Login");
Paragraph subtitle = new Paragraph(
"Please enter the administrator password to access the management interface."
);
VerticalLayout formLayout = new VerticalLayout(title, subtitle, passwordField, loginButton);
formLayout.setSpacing(true);
formLayout.setPadding(true);
formLayout.setAlignItems(Alignment.CENTER);
add(formLayout);
}
The combination of the heading, explanatory paragraph, password field, and button forms exactly matches the compact login panel described in the previous section. Centring the inner VerticalLayout creates a clear focus on the input area, without visual distractions from other UI elements.
The actual login process is encapsulated in attemptLogin(). This is where configuration and user input are merged:
private void attemptLogin() {
if (! LoginConfig.isLoginEnabled()) {
Notification.show(
"Login is currently disabled. Please check the server configuration."
3000,
Notification.Position.MIDDLE
);
UI.getCurrent().navigate(OverviewView.PATH);
return;
}
char[] input = passwordField.getValue() != null
? passwordField.getValue().toCharArray()
: new char[0];
if (! LoginConfig.isLoginConfigured()) {
Notification.show(
"Login is not configured. Please verify that the configuration file has been loaded.",
3000,
Notification.Position.MIDDLE
);
return;
}
boolean authenticated = LoginConfig.matches(input);
if (authenticated) {
SessionAuth.markAuthenticated();
UI.getCurrent().navigate(OverviewView.PATH);
} else {
passwordField.setErrorMessage("Incorrect password");
passwordField.setInvalid(true);
}
}
First, it is checked whether the login is enabled in the configuration. If this is not the case, the user receives clear feedback via notification and is redirected directly to the overview page. The next step is to check whether the login has been configured correctly. Only when both conditions are met is the entered password passed to LoginConfig.matches. If successful, the session is marked as authenticated via SessionAuth.markAuthenticated() and forwarded to the overview. In the event of an error, the view marks the password field as invalid and displays a clear error message – the user remains on the login page and can adjust the input.
Finally, the implementation of the BeforeEnterObserver ensures that the login page itself only remains visible when it makes sense:
@Override
public void beforeEnter(BeforeEnterEvent event) {
If login is disabled, skip the login page entirely
if (! LoginConfig.isLoginEnabled()) {
event.forwardTo(OverviewView.PATH);
return;
}
If already authenticated, also skip the login page
if (SessionAuth.isAuthenticated()) {
event.forwardTo(OverviewView.PATH);
}
}
This prevents authenticated users from being redirected to the login page again and ensures
This prevents authenticated users from being redirected to the login page again and ensures that a globally deactivated login system does not unnecessarily display a redundant password dialogue.
Another aspect is the interaction with the configuration. If login in the auth.properties file is disabled or not configured meaningfully, the view displays clear instructions. In one case, it indicates that login is currently disabled and redirects directly to the overview page. In the other case, it suggests that a valid configuration could not be loaded. The page thus serves not only as an input form, but also as a diagnostic point where misconfigurations become visible at an early stage.

Overall, the new login page ensures that access to the admin interface is structured, predictable and visually clearly distinguished from the rest of the system. It combines a deliberately simple interaction with a comprehensible security concept, thereby laying the foundation for the other building blocks of the login flow, particularly route protection and session management.
Cheers Sven
Discover more from Sven Ruppert
Subscribe to get the latest posts sent to your email.