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 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:
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.
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.
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.
The logout page template is included for completeness.
<!DOCTYPE html>
<html>
<head>
<title>Logout</title>
</head>
<body>
<a href="/hello">Hello</a>
</body>
</html>
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");
}
Return to the tutorials.