/**
* Copyright 2012 55 Minutes (http://www.55minutes.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lt.inventi.wicket.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.form.StatelessForm;
import org.apache.wicket.model.IModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A form that authenticates the user via Shiro when an email and password are
* submitted. Also supports "remember me". Subclasses are expected to provide
* the actual markup and Wicket components for the email and password fields.
* <p>
* Updated by @vadim to support Wicket 6.
*
* @see #getEmailField
* @see #getPasswordField
* @see #remember
* @since 3.0
*/
public abstract class AbstractLoginForm<T> extends StatelessForm<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLoginForm.class);
public AbstractLoginForm(String id) {
super(id);
}
public AbstractLoginForm(String id, IModel<T> model) {
super(id, model);
}
/**
* Returns the email form field component. It is the responsibility of the
* subclass to construct this component, give it a String model, and insert
* it into the component hierarchy according to the desired markup. When the
* form is submitted the model of this email field component will be used as
* the email address for authentication.
*/
protected abstract Component getEmailField();
/**
* Returns the password form field component. It is the responsibility of
* the subclass to construct this component, give it a String model, and
* insert it into the component hierarchy according to the desired markup.
* When the form is submitted the model of this password field component
* will be used as the password for authentication.
*/
protected abstract Component getPasswordField();
/**
* Delegate to {@link #loginShiro loginShiro()} to peform the
* authentication; if it succeeds, redirect to the user's original intended
* destination or to the application home page.
*/
@Override
protected void onSubmit() {
String email = getEmailField().getDefaultModelObjectAsString();
String password = getPasswordField().getDefaultModelObjectAsString();
// Convert email to lowercase just in case the backend authentication system
// is case sensitive and the user accidentally typed in uppercase.
if (email != null) {
email = email.toLowerCase();
}
if (loginShiro(email, password, remember())) {
continueToOriginalDestination();
// if we reach this line there was no intercept page, so go to home page
setResponsePage(getApplication().getHomePage());
}
}
/**
* Peform the actual authentication using Shiro's {@link Subject#login
* login()}.
* <p>
* <b>Important:</b> this method is written to ensure that the user's
* session is replaced with a new session before authentication is
* performed. This is to prevent a <a
* href="https://www.owasp.org/index.php/Session_Fixation">session
* fixation</a> attack. As a side effect, any existing session data will
* therefore be lost.
*
* @return {@code true} if authentication succeeded
*/
protected boolean loginShiro(String email, String password, boolean remember) {
Subject currentUser = SecurityUtils.getSubject();
// Force a new session to prevent fixation attack.
// We have to invalidate via both Shiro and Wicket; otherwise it doesn't work.
currentUser.getSession().stop(); // Shiro
getSession().replaceSession(); // Wicket
UsernamePasswordToken token;
token = new UsernamePasswordToken(email, password, remember);
try {
currentUser.login(token);
return true;
} catch (AuthenticationException ae) {
onAuthenticationException(ae);
}
return false;
}
/**
* Handle any exceptions that are thrown upon login failure by setting an
* appropriate feedback message. The default implemention adds an error
* feedback message with the key {@code loginFailed} to the email field.
*/
protected void onAuthenticationException(AuthenticationException ae) {
LOGGER.debug("Shiro Subject.login() failed", ae);
getEmailField().error(getString("loginFailed", null, "Invalid email and/or password."));
}
/**
* Override this method to return {@code true} if you want to enable Shiro's
* "remember me" feature. By default this returns {@code false}.
*/
protected boolean remember() {
return false;
}
}