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. The simple key pages for this tutorial are as follows:

AuthenticationHttpServer login screen shot.AuthenticationHttpServer hello screen shot.

Tutorial Source

Restricted access page

The page being restricted from access is as follows.

<html>
	<body>
		<p>Hi ${username}</p>
		<br />
		<p><a href="#{logout}">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);
	}

	@Next("LoggedOut")
	public void logout(HttpAuthentication<?> authentication) {
		authentication.logout(null);
	}

}

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 configuration for authentication.

Authentication configuration screen shot.

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). On selecting the authentication scheme, flows necessary for the chosen authentication will be displayed for configuration. In the case of this tutorial, the form login flow and authentication flow are required to be configured to/from the login page. This allows the application to tailor the login page while still being able to re-use the FormHttpSecuritySource.

To enable differing credential stores (e.g. database, LDAP, etc), the WoOF supplied authentication depends on a CredentialStore managed object being configured. The following is the managed object configuration for this tutorial.

<objects>

	<managed-object source="net.officefloor.web.security.store.MockCredentialStoreManagedObjectSource" 
					type="net.officefloor.web.security.store.CredentialStore" />

</objects>

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 is as follows.

<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;
	}

	@FlowInterface
	public static interface Flows {

		void authenticate(HttpCredentials credentials);
	}

	public void login(Form form, Flows flows) {
		flows.authenticate(new HttpCredentialsImpl(form.getUsername(), form.getPassword()));
	}

}

The FormHttpSecuritySource requires the credentials to be provided within a HttpCredentials as a parameter.

Remaining code

The remaining code is included for completeness.

Logout page

<html>
	<head>
		<title>Logout</title>
	</head>
	<body>
		<a href="#{hello}">Hello</a>
	</body>
</html>

Error page

<html>
	<body>
		An error occurred.
	</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+login?username=Daniel&password=Daniel")
				.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("/hello+logout").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.