Fork me on GitHub

Authentication Tutorial

This tutorial looks at configuring authentication.

WoOF provides various authentication schemes along with the ability to customise your own authentication scheme (see HttpSecuritySource for more details). This tutorial will focus on form based authentication.

The below example for this tutorial will demonstrate only allowing a logged in user to view a page.

Tutorial Source

Restricted access page

The template for the restricted page is as follows.

<!DOCTYPE html>
<html>
<body>
    <p th:text="'Hi ' + ${model.username}">Hi username</p>
    <br />
    <p><a href="/logoff">logout</a></p>
</body>
</html>

With the backing logic class.

public class HelloLogic {

	@Data
	public static class TemplateData {

		private final String username;

	}

	@HttpAccess
	public TemplateData getTemplateData(HttpAccessControl accessControl) {
		String username = accessControl.getPrincipal().getName();
		return new TemplateData(username);
	}

	public void logout(HttpAuthentication<?> authentication, ServerHttpConnection connection) throws IOException {
		authentication.logout(null);
		connection.getResponse().setStatus(HttpStatus.SEE_OTHER);
		connection.getResponse().getHeaders().addHeader("location", "/logout");
	}

}

The dependency on HttpSecurity requires the user to be logged in. Should the user not be authenticated, creation of this dependency will cause a AuthenticationRequiredException to be thrown. WoOF automatically handles this exception by:

  1. saving the current request in the HTTP session
  2. send a challenge (in this case sending back the login page)
  3. authenticate the user (in this case validate the entered username and password)
  4. on authenticating the user, continue with the saved request

Since the getTemplateData requires a logged in user the page will not be rendered unless there is a logged in user.

To allow the page to be rendered with or without a logged in user, depend on HttpAuthentication to check if the user is logged in.

Configuring access

The following is the security YAML configuration for authentication.

security:
  source: net.officefloor.web.security.scheme.FormHttpSecuritySource
  properties:
    realm: Test
  flows:
    form: loginRedirect
loginRedirect:
  class: net.officefloor.tutorial.authenticationhttpserver.LoginRedirect

While some authentication schemes are straight forward (e.g. Basic), others such as form based login require application specific behaviour (e.g. a form login page). The flows section wires the FORM_LOGIN_PAGE flow to a redirect function that sends the user to the login page over HTTPS.

The login path is configured to always use HTTPS:

secure: true

To enable differing credential stores (e.g. database, LDAP, etc), the WoOF supplied authentication depends on a CredentialStore managed object being configured. In this case a mock implementation is used that validates the user by ensuring the password matches the username. This is a simple implementation useful for testing.

For production, another CredentialStore should be used. WoOF comes with existing implementations for standard credential stores. Customised implementations may also be used for bespoke environments.

Login page

The login page template is as follows.

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <form action="/login" method="POST">
        Username: <input type="text" name="username" /> <br />
        Password: <input type="password" name="password" /> <br />
        <input type="submit" value="login" />
    </form>
</body>
</html>

With the backing logic class.

public class LoginLogic {

	@Data
	@HttpParameters
	public static class Form implements Serializable {
		private static final long serialVersionUID = 1L;

		private String username;

		private String password;
	}

	public void login(Form form, HttpAuthentication<HttpCredentials> authentication) {
		authentication.authenticate(new HttpCredentialsImpl(form.getUsername(), form.getPassword()), null);
	}

}

The FormHttpSecuritySource requires the credentials to be provided within a HttpCredentials as a parameter. Using HttpAuthentication directly avoids the need for a @FlowInterface and allows authentication to be triggered programmatically.

Remaining code

The logout page template is included for completeness.

<!DOCTYPE html>
<html>
<head>
    <title>Logout</title>
</head>
<body>
    <a href="/hello">Hello</a>
</body>
</html>

Unit Test

The unit test demonstrates logging in and logging out.

	@RegisterExtension
	public final MockWoofServerExtension server = new MockWoofServerExtension();

	private WritableHttpCookie session;

	@Test
	public void login() throws Exception {

		// Ensure require login to get to page
		MockHttpResponse loginRedirect = this.server.send(MockHttpServer.mockRequest("/hello"));
		assertEquals(303, loginRedirect.getStatus().getStatusCode(), "Ensure redirect");
		loginRedirect.assertHeader("location", "https://mock.officefloor.net/login");

		// Obtain the session cookie
		this.session = loginRedirect.getCookie(HttpSessionManagedObjectSource.DEFAULT_SESSION_ID_COOKIE_NAME);

		// Login
		MockHttpRequestBuilder loginRequest = MockHttpServer
				.mockRequest("/login?username=Daniel&password=Daniel")
				.method(net.officefloor.server.http.HttpMethod.POST).secure(true)
				.cookie(this.session.getName(), this.session.getValue());
		MockHttpResponse loggedInRedirect = this.server.send(loginRequest);
		assertEquals(200, loggedInRedirect.getStatus().getStatusCode(),
				"Ensure successful login: " + loggedInRedirect.getEntity(null));

		// Ensure now able to access hello page
		MockHttpResponse helloPage = this.server
				.send(MockHttpServer.mockRequest("/hello").cookie(this.session.getName(), this.session.getValue()));
		String helloPageContent = helloPage.getEntity(null);
		assertEquals(200, helloPage.getStatus().getStatusCode(), "Should obtain hello page: " + helloPageContent);
		assertTrue(helloPageContent.contains("<p>Hi Daniel</p>"), "Ensure hello page with login: " + helloPageContent);
	}

	@Test
	public void logout() throws Exception {

		// Login
		this.login();

		// Logout
		MockHttpResponse logoutRedirect = this.server.send(
				MockHttpServer.mockRequest("/logoff").cookie(this.session.getName(), this.session.getValue()));
		assertEquals(303, logoutRedirect.getStatus().getStatusCode(),
				"Ensure logout: " + logoutRedirect.getEntity(null));
		logoutRedirect.assertHeader("location", "/logout");

		// Attempt to go back to page (but require login)
		MockHttpResponse loginPage = this.server
				.send(MockHttpServer.mockRequest("/hello").cookie(this.session.getName(), this.session.getValue()));
		assertEquals(303, loginPage.getStatus().getStatusCode(), "Ensure redirect");
		loginPage.assertHeader("location", "https://mock.officefloor.net/login");
	}

Next

Return to the tutorials.