Simple HTML Form
There are a lot of ways to secure your application but if you propose a UI it will likely start with a HTML form.
Let's see how to create a super trivial implementation using the root code in the browser and a simple JAX-RS endpoint. Goal of that example is to show what is involved for such cases and let you bind your own stack more easily.
Create the backend
To keep it simple we'll use JAX-RS to implement the backend but you basically just need a way to get the username and password and validate it.
Note: a login can require additional information like a token to ensure you are not an attacker for instance. This post doesn't cover it but the technology is exactly the same except you would pass the token in a hidden way (either an input or other storage).
The easiest way to send data through a HTML form is to use application/x-www-form-urlencoded media type, it will basically send the form as a request query but in the payload. The interest is to not let the password be in the URL which could be a security leakage.
Concretely your payload will look like:
username=whoami&password=thisisnotvisible
Of course Servlet (and therefore JAX-RS) abstract that and to parse it you just need to read the parameters. In JAX-RS land it is mapped to a payload of type MultivaluedMap.
To be concrete the endpoint signature looks like:
@Path("login")
@ApplicationScoped
public class LoginResource {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public User doLogin(final MultivaluedMap<String, String> form) {
final String username = form.getFirst("username");
final String password = form.getFirst("password");
// will be done soon
}
}
Now the question is: how to validate the user? You have several options there but the two big are:
- custom mecanism
- use your container solution/realm
The first one would lead to:
?
@Path("login")
@ApplicationScoped
public class LoginResource {
@Inject
private UserRepository users;
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public User doLogin(@Context final HttpServletRequest request,
final MultivaluedMap<String, String> form) {
final String username = form.getFirst("username");
final String password = form.getFirst("password");
if (users.findByUserAndPassword(username, password).isPresent()) {
// success
} else {
// failed
}
}
}
?
The issue in such a case is you get the full responsability of the security in the application. It is fine if you handle yourself the deployment but less if you provide a solution installed by someone else who can desire to get control on that part. Also hidden in that snippet: you need to NOT store the password in clear and a common strategy is to hash N times the password combined with a salt. All this code belongs to you (and your application) if you use a custom user repository.
Alternatively you can push it back to the container (and let it be plugged by the "deployers") and just use Servlet 3.0 login API:
@Path("login")
@ApplicationScoped
public class LoginResource {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public User doLogin(@Context final HttpServletRequest request,
final MultivaluedMap<String, String> form) {
final String username = form.getFirst("username");
final String password = form.getFirst("password");
try {
request.login(username, password);
// success
} catch (final ServletException e) {
// failure
}
}
}
This is great but will just require a small addition in case you use a stateless login mecanisms (= not using the session). It is for instance the case for JWT which shouldn't require any HTTP session (and therefore scale very well). It is also the case - theorically - for any "database" - SQL or NoSQL - token based solutions but then it depends your implementation. These use cases will require to clean the container security context as soon as you don't need it anymore. What does it mean? Just ensure to logout once you don't need the principal anymore to release the security context from the container:
?
@Path("login")
@ApplicationScoped
public class LoginResource {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public User doLogin(@Context final HttpServletRequest request,
final MultivaluedMap<String, String> form) {
final String username = form.getFirst("username");
final String password = form.getFirst("password");
try {
request.login(username, password);
// success
} catch (final ServletException e) {
// failure
} finally {
if (request.getUserPrincipal() != null) { // if stateless
try {
request.logout();
} catch (final ServletException e) {
// no-op
}
}
}
}
}
Now we just miss the success/failure code. The success is quite easy: just return a client context the application can use to show contextual data. Typically the displayname or at least username ("Hello XXX" in the UI). For error case just needs to throw a HTTP status translating this case, a status of 401 (unauthorize) is good enough for us:
@Path("login")
@ApplicationScoped
public class LoginResource {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public User doLogin(@Context final HttpServletRequest request,
final MultivaluedMap<String, String> form) {
final String username = form.getFirst("username");
final String password = form.getFirst("password");
try {
request.login(username, password);
return new User(username);
} catch (final ServletException e) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
} finally {
if (request.getUserPrincipal() != null) {
try {
request.logout();
} catch (final ServletException e) {
// no-op
}
}
}
}
}
The HTML form
The HTML form will be easy for this case:
- a username
- a password
- a submit/login button
- a zone where we can show errors
Here is what it can look like:
<form action="#" method="post">
<div class="error"></div>
Username:<input type="text" name="username"><br>
Password:<input type="password" name="password"><br>
<button type="submit">Login</button>
</form>
Here what it look like:
This is simple but looks like what we want right? It actually does nothing interesting except reloading the page on click (cause action path is # and not an actual url/path).
What we want on the submit is to:
- validate the user through our endpoint
- redirect on a valid login to a /home.html page
To do that we'll bind a javascript action to the login button:
<button type="submit" onclick="doSubmit();return false;">Login</button>
Now doSubmit() will be called when we'll click on that button. The return false is just there to stop the javascript event loop once we executed our hook doSubmit().
We'll add to our HTML page a javascript block (in real projects we would do it in a login.js file but for that demo it is fine to put it all together). This block will be responsible to define the doSubmit() function which will send our POST request to our server and redirect to /home.html if the login was successful and return a HTTP 200 (OK) status or just add in the div block intended to show errors in our form some message if it failed to let the user know he has to resubmit the form:
<script type="text/javascript">
function doSubmit() {
// 1
var form = document.getElementsByTagName('form')[0];
var username = form.children[1].value;
var password = form.children[3].value;
// 2
var request = new XMLHttpRequest();
request.open('POST', 'api/login', true);
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
request.onreadystatechange = function () {
// 4
if (this.readyState !== 4) {
return;
}
// 5
if (this.status === 200) { // HTTP_OK
// 6
console.log(JSON.parse(this.responseText).username);
document.location.pathname = '/home.html';
} else {
// 7
form.children[0].innerHTML = 'Error: ' + this.responseText;
}
};
// 3
request.send('username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password));
};
</script>
- we get the form DOM element (HTML element) and extract from its children the two input values representing our username and password
- we create a HTTP request of type POST on the endpoint api/login and listen for the status changes (request initialized, request received etc... )
- We send our payload in urlencoded format based on the data we extracted from the form to actually login our user
- readyState represents the state of the request, only 4 is interesting for us and means the request is done
- request is done so we can postprocess the response
- if the status is OK then we can extract the username (here we just log it but that's to show how to extract it, in a real project we would put it in the browser session or any client storage) and finally we redirect the application on /home.html
- if the login failed we add in the error block a message. Note this code will show the actual failure instead of a nice error message but the principle would be the same to do something more user friendly, you would just read the message in another manner.
Going further
This solution works but misses several points to be usable in a real project like:
- a correct error handling on the client side
- likely some theming
- a servlet Filter to validate the security on all secured backend calls
- this likely requires to send back a token in the login endpoint and propagate it from the client for all secured calls
- some actual container realm setup to login against a database, ldap or any other source
- some higher level javascript code
- you can also investigate servlet FORM solution (which is not that compatible with SPA and stateless state but depending the application it can worth it if it is not a constraint)
- ...
This post was intended to present the basis of a login solution and will not detail all these points but just to give you an idea, the last point (javascript code level) will make it really simpler since you generally have a http service in any modern javascript framework abstracting the XMLHttpRequest. Typically with Angular you will do:
login(username, password) {
return this.http.post('security/login', {username: username, password: password})
.map(response => {
this.initToken(credentials.username, response.token, response.rememberMe)
return res;
});
}
This (typescript) function will do the post request in JSON format - but urlencoded one is available as well - and then once the response is there it will initialize the client security context. If you compare it with the raw javascript code you don't need to handle the XMLHttpRequest state, you don't need to handle the JSON parsing or creation yourself etc...however you will need to setup angular since it is not part of the browser language. However, even if the getting started steps can look very costly (you need to setup node/npm, maybe integrate it with Maven through frontend-maven-plugin etc...), you will see that once done the development flow is quite smooth and the code way more maintainable.
Conclusion
A login form is always the starting point of setting up security for your application. However it is only the first step and just the entry point to enable security.
For the webapp case (this post topic) it also requires to understand it integrates two technologies and enable the communication between those two parts of the application.
From the same author:
In the same category: