jsr375-experts@javaee-security-spec.java.net

[jsr375-experts] Re: FORM authentication mechanism implemented

From: arjan tijms <arjan.tijms_at_gmail.com>
Date: Mon, 21 Mar 2016 18:46:36 +0100

Hi,

On Mon, Mar 14, 2016 at 6:32 PM, Rudy De Busscher <rdebusscher_at_gmail.com>
wrote:

>
> Of course, a version where you can define the Form yourself (Like JSF) and use the *request.authenticate()* is very usefull.
>
> I just implemented this ;)

The corresponding authentication mechanism looks like this:

@AutoApplySession
@LoginToContinue
@Typed(CustomFormAuthenticationMechanism.class)
public class CustomFormAuthenticationMechanism implements
HttpAuthenticationMechanism, LoginToContinueHolder {
    private LoginToContinue loginToContinue;

    @Inject
    private IdentityStore identityStore;

@Override
public AuthStatus validateRequest(HttpServletRequest request,
HttpServletResponse response, HttpMessageContext httpMessageContext) throws
AuthException {

        if (hasCredential(httpMessageContext)) {
            return httpMessageContext.notifyContainerAboutLogin(
                identityStore.validate(
                    httpMessageContext.getAuthParameters()
                                      .getCredential()));
        }
return httpMessageContext.doNothing();
}
}

See:
https://github.com/javaee-security-spec/soteria/blob/master/impl/src/main/java/org/glassfish/soteria/mechanisms/CustomFormAuthenticationMechanism.java


So the core of the mechanism is quite thin; it just grabs the credential
from the programmatic authentication request and passes it to the identity
store.

The test application uses a JSF form:

    <body>

         <form jsf:id="form">
            <p>
                <strong>Username </strong>
                <input type="text" jsf:value="#{loginBacking.username}" />
            </p>
            <p>
                <strong>Password </strong>
                <input type="password" jsf:value="#{loginBacking.password}"
/>
            </p>
            <p>
                <input type="submit" value="Login"
jsf:action="#{loginBacking.login}" />
            </p>
        </form>

    </body>

See:
https://github.com/javaee-security-spec/soteria/blob/master/test/app-mem-customform/src/main/webapp/login.xhtml


The form isn't posted back to a special URL, but just to the normal one.
That way JSF/BeanValidation etc can be done.

Finally, the backing bean triggers (continues) the authentication chain:

@Named
@RequestScoped
public class LoginBacking {

    @Inject
    private SecurityContext securityContext;

    @NotNull
    @Size(min = 3, max = 15, message="Username must be between 3 and 15
characters")
    private String username;

    @NotNull
    @Size(min = 5, max = 50, message="Password must be between 5 and 50
characters")
    private String password;

    public void login() {

        FacesContext context = FacesContext.getCurrentInstance();
        Credential credential = new UsernamePasswordCredential(username,
new Password(password));

        AuthStatus status = securityContext.authenticate(
            getResponse(context),
            withParams()
                .credential(credential));

        if (status.equals(SEND_CONTINUE)) {
            // Authentication mechanism has send a redirect, should not
            // send anything to response from JSF now.
            context.responseComplete();
        } else if (status.equals(FAILURE)) {
            addError(context, "Authentication failed");
        }

    }

See
https://github.com/javaee-security-spec/soteria/blob/master/test/app-mem-customform/src/main/java/test/LoginBacking.java

There are some things to remark here.

I went ahead and created a minimum version of the SecurityContext that just
contains the authenticate() method. The implementation of this eventually
calls the well known HttpServletRequest#authenticate method, but it
(perhaps unfortunately) needs some setup before this.

HttpServletRequest#authenticate is a little problematic:

1. It doesn't allow for parameters to be passed

2. The return value is a bit unclear. It's true/false and exception, which
mean respectively "authenticated", "in progress" and "not authenticated"

3. JBoss and GlassFish use the return differently. In both servers "true"
is "authenticated", but for GlassFish "in progress" and "not authenticated"
are both indicated by "false", while in JBoss they are distinguished. I
think GF violates the spec here, but have to double check.

4. There's no indication for the authentication mechanism that
HttpServletRequest#authenticate was used. For the mechanism it just looks
like any request to a protected resource. In this case this information is
needed since we assume authenticate() is called to *continue* an
authentication dialog, not to start a *new* one. Maybe we should make this
explicit via the API somehow. There's an existing JASPIC issue for this as
well. See https://java.net/jira/browse/JASPIC_SPEC-5

I also went ahead and defined and implemented a class for the parameters
called AuthenticationParameters. I'm not 100% sure about this yet.

Additionally the SecurityContext#authenticate method now wants to have the
response object (since HttpServletRequest#authenticate wants this), so that
has to be passed in by the caller.

It would perhaps be nicer if the SecurityContext could use a default
HttpServletResponse, but at the moment this is not available in the Java EE
platform. I don't think it should be the security API's job to make this
available though (Servlet would be better, but it may be hard to ask this
from them now).

Finally, I re-used the JASPIC AuthStatus code again, but I think we'd be
better off to have our own enumeration instead.

All in all, the above works, but it has some rough edges and quite some
details in the API design still have to be discussed I guess.

Kind regards,
Arjan Tijms












































> But the developer can always define it like this because it is part of the 'custom way of implementation'? I guess.
>
> regards
>
> Rudy
>
>
>
> On 14 March 2016 at 17:05, Ivar Grimstad <ivar.grimstad_at_gmail.com> wrote:
>
>>
>>
>> On Mon, Mar 14, 2016 at 4:45 PM arjan tijms <arjan.tijms_at_gmail.com>
>> wrote:
>>
>>> Hi,
>>>
>>> On Mon, Mar 14, 2016 at 4:15 PM, Ivar Grimstad <ivar.grimstad_at_gmail.com>
>>> wrote:
>>>
>>>> Hi Arjan,
>>>>
>>>> It looks great! Thanks!
>>>>
>>>
>>> Thanks ;)
>>>
>>>
>>>> Should the form name and parameter names be configurable with defaults
>>>> j_security_check, j_username and j_password? Or are this names so common to
>>>> use that they can be considered standard? WDYT?
>>>>
>>>
>>> For the Servlet FORM mechanism they are hardcoded for now indeed, but
>>> the two interceptors used, @AutoApplySession and @LoginToContinue are
>>> designed to be re-used for other mechanisms, which can behave largely
>>> differently.
>>>
>>> So two options here ;) Make them configurable, or have a second
>>> "extended form" mechanism.
>>>
>>
>>>
>> At any length I wanted to code up a second mechanism that is largely what
>>> FORM does, only depending on request.authenticate() instead of a POST to
>>> j_security_check. With that you can create your own form and have JSF (or
>>> whatever) validations, but still have the automatic remember and forward to
>>> a login page whenever you try to access a protected page without being
>>> logged-in.
>>>
>>>
>>> I ported your servlet example to MVC 1.0. It works as a charm :)
>>>> Since there are no Servlet, the
>>>> @ServletSecurity(@HttpConstraint(rolesAllowed = "foo")) does not kick in
>>>> and this currently needs to be configured in web.xml, but I guess this will
>>>> be solved when we start looking at the Role/Permission Assignment epic.
>>>>
>>>
>>> In the case of MVC, it's mainly the @RolesAllowed (or variant on that)
>>> CDI interceptor, right? Or did you had something else in mind here?
>>>
>>
>> Yes, that is what I meant.
>>
>>
>>>
>>>
>>>> My sample code is here:
>>>> https://github.com/ivargrimstad/security-samples/tree/master/form-embedded-mvc
>>>>
>>>
>>> Nice one ;)
>>>
>>> I was doubting a little btw about this construct now:
>>>
>>> @FormAuthenticationMechanismDefinition(
>>> loginToContinue = @LoginToContinue(
>>> loginPage = "/ui/login",
>>> errorPage = "/ui/login?auth=-1"))
>>>
>>> It now re-uses the @LoginToContinue annotation. Variants are:
>>>
>>> @FormAuthenticationMechanismDefinition(
>>> @LoginToContinue(
>>> loginPage = "/ui/login",
>>> errorPage = "/ui/login?auth=-1"))
>>>
>>> and
>>>
>>> @FormAuthenticationMechanismDefinition(
>>> loginPage = "/ui/login",
>>> errorPage = "/ui/login?auth=-1")
>>>
>>> Last one looks a bit nicer, but since the FORM mechanism re-uses
>>> @LoginToContinue you'd have to keep the two in sync. Not sure yet what is
>>> best...
>>>
>>
>> Yes, the last one looks a little better. I think developers will
>> appreciate less typing, even if the IDEs give you code completion...
>>
>>
>>>
>>> Kind regards,
>>> Arjan Tijms
>>>
>>>
>>>
>>>
>>>
>>>
>>>
>>>>
>>>>
>>>> Ivar
>>>>
>>>> On Mon, Mar 14, 2016 at 12:59 AM arjan tijms <arjan.tijms_at_gmail.com>
>>>> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> I just implemented a clone of Servlet's FORM authentication mechanism
>>>>> using the proposed JSR 375 API. A demo app showing it can be found at
>>>>> https://github.com/javaee-security-spec/soteria/tree/master/test/app-mem-form
>>>>>
>>>>> The mechanism itself looks like this:
>>>>>
>>>>> @AutoApplySession
>>>>> @LoginToContinue
>>>>> @Typed(FormAuthenticationMechanism.class)
>>>>> public class FormAuthenticationMechanism implements
>>>>> HttpAuthenticationMechanism, LoginToContinueHolder {
>>>>> private LoginToContinue loginToContinue;
>>>>>
>>>>> @Inject
>>>>> private IdentityStore identityStore;
>>>>>
>>>>> @Override
>>>>> public AuthStatus validateRequest(HttpServletRequest request,
>>>>> HttpServletResponse response, HttpMessageContext httpMessageContext) throws
>>>>> AuthException {
>>>>> if ("POST".equals(request.getMethod()) &&
>>>>> request.getRequestURI().endsWith("/j_security_check")) {
>>>>>
>>>>> if (notNull(request.getParameter("j_username"),
>>>>> request.getParameter("j_password"))) {
>>>>>
>>>>> CredentialValidationResult result = identityStore.validate(
>>>>> new UsernamePasswordCredential(
>>>>> request.getParameter("j_username"),
>>>>> new
>>>>> Password(request.getParameter("j_password"))));
>>>>>
>>>>> if (result.getStatus() == VALID) {
>>>>> return httpMessageContext.notifyContainerAboutLogin(
>>>>> result.getCallerPrincipal(),
>>>>> result.getCallerGroups());
>>>>> } else {
>>>>> throw new AuthException("Login failed");
>>>>> }
>>>>> }
>>>>> }
>>>>> return httpMessageContext.doNothing();
>>>>> }
>>>>>
>>>>> See
>>>>> https://github.com/javaee-security-spec/soteria/blob/master/impl/src/main/java/org/glassfish/soteria/mechanisms/FormAuthenticationMechanism.java
>>>>>
>>>>>
>>>>> This leans heavily on the @AutoApplySession and @LoginToContinue
>>>>> interceptors, which can both be re-used by other mechanisms,
>>>>>
>>>>> The heart of the @LoginToContinue interceptor contains this code:
>>>>>
>>>>> private AuthStatus validateRequest(InvocationContext
>>>>> invocationContext, HttpServletRequest request, HttpServletResponse
>>>>> response, HttpMessageContext httpMessageContext) throws Exception {
>>>>>
>>>>> if (isOnProtectedURLWithStaleData(httpMessageContext)) {
>>>>> removeSavedRequest(request);
>>>>> }
>>>>>
>>>>> if (isOnInitialProtectedURL(httpMessageContext)) {
>>>>> saveRequest(request);
>>>>>
>>>>> return httpMessageContext.forward(
>>>>>
>>>>> getLoginToContinueAnnotation(invocationContext).loginPage());
>>>>> }
>>>>>
>>>>> if (isOnLoginPostback(request)) {
>>>>> AuthStatus authstatus = null;
>>>>>
>>>>> try {
>>>>> authstatus = (AuthStatus) invocationContext.proceed();
>>>>> } catch (AuthException e) {
>>>>> authstatus = FAILURE;
>>>>> }
>>>>>
>>>>> if (authstatus == SUCCESS) {
>>>>>
>>>>> if (httpMessageContext.getCallerPrincipal() == null) {
>>>>> return SUCCESS;
>>>>> }
>>>>>
>>>>> RequestData savedRequest = getSavedRequest(request);
>>>>>
>>>>> if (!savedRequest.matchesRequest(request)) {
>>>>> saveAuthentication(request, new
>>>>> CredentialValidationResult(
>>>>> VALID,
>>>>> httpMessageContext.getCallerPrincipal(),
>>>>> httpMessageContext.getGroups()));
>>>>>
>>>>> return
>>>>> httpMessageContext.redirect(savedRequest.getFullRequestURL());
>>>>> } // else return success
>>>>>
>>>>> } else {
>>>>> return httpMessageContext.redirect( // TODO: or
>>>>> forward?
>>>>> getBaseURL(request) +
>>>>>
>>>>> getLoginToContinueAnnotation(invocationContext).errorPage());
>>>>> }
>>>>> }
>>>>>
>>>>> if (isOnOriginalURLAfterAuthenticate(request)) {
>>>>> return httpMessageContext
>>>>> .withRequest(new HttpServletRequestDelegator(request,
>>>>> requestData))
>>>>> .notifyContainerAboutLogin(
>>>>> result.getCallerPrincipal(),
>>>>> result.getCallerGroups());
>>>>> }
>>>>>
>>>>> return httpMessageContext.doNothing();
>>>>> }
>>>>>
>>>>> See:
>>>>> https://github.com/javaee-security-spec/soteria/blob/master/impl/src/main/java/org/glassfish/soteria/cdi/LoginToContinueInterceptor.java
>>>>>
>>>>>
>>>>> FORM is a rather nasty mechanism to implement, and going for the
>>>>> re-usable parts took a little extra time, but it does show that the
>>>>> proposed mechanism API is capable of implementing a quite demanding set of
>>>>> requirements (FORM is really surprisingly demanding for such an old
>>>>> mechanism).
>>>>>
>>>>> Do note that both design and implementation are rather rough at the
>>>>> moment and could do with some refinements still. But I think this could
>>>>> work for an early draft.
>>>>>
>>>>> Kind regards,
>>>>> Arjan Tijms
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>