/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.wicket.security.login.http;
import javax.servlet.http.HttpServletResponse;
import org.apache.wicket.Application;
import org.apache.wicket.IPageMap;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.Session;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.pages.AccessDeniedPage;
import org.apache.wicket.model.IModel;
import org.apache.wicket.protocol.http.WebRequest;
import org.apache.wicket.protocol.http.WebResponse;
import org.apache.wicket.security.WaspSession;
import org.apache.wicket.security.authentication.LoginException;
import org.apache.wicket.security.strategies.WaspAuthorizationStrategy;
import org.apache.wicket.util.crypt.Base64;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Login Page that uses httpauthentication to login. Currently it only supports Basic Http
* authentication as defined in RFC 2616 section 14.47 HTTP 1.1. But the way it is setup
* it should be able to support addition protocols like RFC 2617. Thanks go to Jesse
* Barnum and Johan Compagner.
*
* @author marrink
* @see <a href="http://tools.ietf.org/html/rfc2616#section-14.47">rfc2616</a>
*/
public abstract class HttpAuthenticationLoginPage extends WebPage
{
private static final Logger log = LoggerFactory.getLogger(HttpAuthenticationLoginPage.class);
private boolean doAuthentication = false;
/**
* Basic constructor.
*
* @see WebPage#WebPage()
*/
public HttpAuthenticationLoginPage()
{
}
/**
* Constructor.
*
* @param model
* @see WebPage#WebPage(IModel)
*/
protected HttpAuthenticationLoginPage(IModel< ? > model)
{
super(model);
}
/**
* Constructor.
*
* @param pageMap
* @see WebPage#WebPage(IPageMap)
*/
protected HttpAuthenticationLoginPage(IPageMap pageMap)
{
super(pageMap);
}
/**
* Constructor.
*
* @param parameters
* @see WebPage#WebPage(PageParameters)
*/
protected HttpAuthenticationLoginPage(PageParameters parameters)
{
super(parameters);
}
/**
* Constructor.
*
* @param pageMap
* @param model
* @see WebPage#WebPage(IPageMap, IModel)
*/
protected HttpAuthenticationLoginPage(IPageMap pageMap, IModel< ? > model)
{
super(pageMap, model);
}
/**
* @see org.apache.wicket.markup.html.WebPage#configureResponse()
*/
@Override
protected void configureResponse()
{
super.configureResponse();
if (doAuthentication)
{
if (getWebRequestCycle().getResponse() instanceof WebResponse)
{
WebResponse response = getWebRequestCycle().getWebResponse();
WebRequest request = getWebRequestCycle().getWebRequest();
String auth = request.getHttpServletRequest().getHeader("Authorization");
if (Strings.isEmpty(auth))
requestAuthentication(request, response);
else
{
int index = auth.indexOf(' ');
if (index < 1)
requestAuthentication(request, response);
String type = auth.substring(0, index);
try
{
handleAuthentication(request, response, type, auth.substring(index + 1));
}
catch (LoginException e)
{
log.error(type + " Http authentication failed", e);
error(e);
requestAuthentication(request, response);
}
}
}
}
}
/**
* Sets a flag to handle authentication headers and sets response to request
* authentication if required. This method needs to be called manually. I recommend 1
* of these 2 locations to call this method from.<br/>
* <ol>
* <li>from within the constructor</li>
* <li>from within a {@link Link#onClick()} or {@link Form#onSubmit()}</li>
* </ol>
* The benefit of the second method is that you can render the login page at least
* once before activating the http authentication by the click of a button. Using the
* first option, this page is not shown by the browser.
*/
protected final void doAuthentication()
{
doAuthentication = true;
}
/**
* Sets the statuscode of the response to 401. setting headers is delegated to
* {@link #addBasicHeaders(WebRequest, WebResponse)}. Subclasses should override this
* method to set their custom headers.
*
* @param request
* @param response
*/
protected void requestAuthentication(WebRequest request, WebResponse response)
{
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_UNAUTHORIZED);
addBasicHeaders(request, response);
// add more "WWW-Authenticate" headers as required
}
/**
* Adds a "WWW-Authenticate" header for basic authentication to the response.
*
* @param request
* @param response
*/
protected void addBasicHeaders(WebRequest request, WebResponse response)
{
response.getHttpServletResponse().addHeader("WWW-Authenticate",
"Basic realm=\"" + getRealm(request, response) + "\"");
}
/**
* The authentication realm. The realm is required by the authentication headers and
* will be shown by the browser.
*
* @param request
* @param response
*
* @return the realm
*/
public abstract String getRealm(WebRequest request, WebResponse response);
/**
* Delegates authentication. Subclasses should first try there custom authentication
* scheme before letting super handle the call. Subclasses should either return a
* boolean value (see
* {@link #handleBasicAuthentication(WebRequest, WebResponse, String, String)} ) if
* processing should continue or throw an exception.
*
* @param request
* @param response
* @param scheme
* the authentication scheme like "Basic" or "Digest"
* @param param
* the parameters after the scheme from the header
* @throws LoginException
* if the user could not be logged in.
* @throws RestartResponseAtInterceptPageException
* to an {@link AccessDeniedPage} if the scheme is not supported
*/
protected void handleAuthentication(WebRequest request, WebResponse response, String scheme,
String param) throws LoginException
{
if (!handleBasicAuthentication(request, response, scheme, param))
return;
log.error("Unsupported Http authentication type: " + scheme);
throw new RestartResponseAtInterceptPageException(Application.get()
.getApplicationSettings().getAccessDeniedPage());
}
/**
* Handles authentication for the "Basic" scheme. If the scheme is not the basic
* scheme true is returned so another implementation may try it. In general
* authentication attempts by the next scheme should only proceed if the scheme was of
* the wrong type. False will generally be returned when a) the user has been
* authenticated or b) the scheme is correct but another problem arises, like missing
* additional headers.
*
* @param request
* @param response
* @param scheme
* @param param
* username:password in base 64
* @return true if authentication by another scheme should be attempted, false if
* authentication by another scheme should not be attempted.
* @throws LoginException
* If the supplied credentials do not grant enough credits for the
* requested resource
* @throws RestartResponseAtInterceptPageException
* to the home page if the login was successfull but when there is no page
* to continue to.
*/
protected boolean handleBasicAuthentication(WebRequest request, WebResponse response,
String scheme, String param) throws LoginException
{
if (!"Basic".equalsIgnoreCase(scheme))
return true;
if (param == null)
{
log.error("Username, password not supplied");
return false;
}
byte[] decoded = Base64.decodeBase64(param.getBytes());
String[] split = new String(decoded).split(":");
if (split == null || split.length != 2)
throw new LoginException("Could not decrypt username / password");
Object loginContext = getBasicLoginContext(split[0], split[1]);
Session session = Session.get();
if (session instanceof WaspSession)
{
if (!isAuthenticated())
((WaspSession) session).login(loginContext);
if (!continueToOriginalDestination())
{
throw new RestartResponseAtInterceptPageException(Application.get().getHomePage());
}
}
else
log.error("Unable to find WaspSession");
return false;
}
/**
* Check if already someone is authenticated to prevent duplicate logins. By default
* this checks if the home page is authenticated.
*
* @return true if the user is already authenticated, false otherwise
*/
protected boolean isAuthenticated()
{
WaspAuthorizationStrategy strategy =
(WaspAuthorizationStrategy) Session.get().getAuthorizationStrategy();
return strategy.isClassAuthenticated(Application.get().getHomePage());
}
/**
* Delivers a context suitable for logging in with the specified username and
* password. Please refer to your specific wasp implementation for a suitable context.
*
* @param username
* @param password
* @return the login context or null if none could be created
*/
protected abstract Object getBasicLoginContext(String username, String password);
}