/*
* 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 java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.WicketRuntimeException;
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.util.crypt.Base64;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.lang.PropertyResolver;
import org.apache.wicket.util.lang.PropertyResolverConverter;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ==WARNING, This class is not production ready, and as of yet contains a serious bug
* preventing this class from doing its intended task.== Login page that uses
* httpauthentication to login. This class adds support for the digest scheme. RFC 2617.
* Support for the basic scheme is configurable. Before you use this class you should at
* least understand the principals behind digest authentication. A few notes:<br />
* <ul>
* <li>if you want to use qop=auth-int you need to do this yourself in
* {@link #getQop(WebRequest, WebResponse)} and
* {@link #getA2Value(org.apache.wicket.security.login.http.HttpDigestLoginPage.DigestAuthorizationRequestHeader, WebRequest)}
* .</li>
* <li>You need to take care of storing the checksum H(A1) if you use the MD5-sess
* algorithm</li>
* <li>You need either the value of H(A1) for the specified algorithm or the clear text
* password of the user</li>
* <li>No Authentication-Info header is send unless you do so in
* {@link #addDigestHeaders(WebRequest, WebResponse)}, this means only a single nonce is
* used, see also {@link #validateNonceCount(String, String)}</li>
* </ul>
*
* @author marrink
* @see <a href="http://tools.ietf.org/html/rfc2617">rfc2617</a>
*/
public abstract class HttpDigestLoginPage extends HttpAuthenticationLoginPage
{
private static final Logger log = LoggerFactory.getLogger(HttpDigestLoginPage.class);
/**
* Matches recurring patterns like : key="some value" or key=value separated by a
* comma (,). groups are as following 1:key-value pair, 2:key, 3:value without quotes
* if value was quoted, 4: value if value was not quoted.
*/
private static final Pattern HEADER_FIELDS =
Pattern.compile("(([a-zA-Z]+)=(?:\"([\\p{Graph}\\p{Blank}]+?)\"|([^\\s\",]+)))+,?");
private boolean allowBasicAuthenication = true;
/**
* If the MD5-sess algorithm is used the hash value is only calculated once. The
* results of the checksum of this hash are stored in this value. The spec calls this
* H(A1).
*
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.2.2">section 3.2.2.2
* (A1)</a>
*/
private String a1ChecksumForMD5Sess;
/**
* The nonceCount of the server. Please build your own support for multiple nonces.
*/
private int nonceCount;
/**
* Construct.
*/
public HttpDigestLoginPage()
{
}
/**
* Construct.
*
* @param model
*/
public HttpDigestLoginPage(IModel< ? > model)
{
super(model);
}
/**
* Construct.
*
* @param pageMap
*/
public HttpDigestLoginPage(IPageMap pageMap)
{
super(pageMap);
}
/**
* Construct.
*
* @param parameters
*/
public HttpDigestLoginPage(PageParameters parameters)
{
super(parameters);
}
/**
* Construct.
*
* @param pageMap
* @param model
*/
public HttpDigestLoginPage(IPageMap pageMap, IModel< ? > model)
{
super(pageMap, model);
}
/**
* @see org.apache.wicket.security.login.http.HttpAuthenticationLoginPage#handleAuthentication(org.apache.wicket.protocol.http.WebRequest,
* org.apache.wicket.protocol.http.WebResponse, java.lang.String,
* java.lang.String)
*/
@Override
protected void handleAuthentication(WebRequest request, WebResponse response, String scheme,
String param) throws LoginException
{
if (!handleDigestAuthentication(request, response, scheme, param))
return;
if (isAllowBasicAuthenication())
super.handleAuthentication(request, response, scheme, param);
else
{
log.error("Unsupported Http authentication type: " + scheme);
throw new RestartResponseAtInterceptPageException(Application.get()
.getApplicationSettings().getAccessDeniedPage());
}
}
/**
* Handles authentication for the "Digest" scheme. If the scheme is not the digest
* scheme true is returned so another implementation may try it
*
* @param request
* @param response
* @param scheme
* @param param
* @return true if authentication by another scheme should be attempted, false if
* authentication by another scheme should not be attempted.
* @throws LoginException
* if the user can not be logged in
*/
protected boolean handleDigestAuthentication(WebRequest request, WebResponse response,
String scheme, String param) throws LoginException
{
if (!"Digest".equalsIgnoreCase(scheme))
return true;
if (param == null)
{
log.error("Digest headers not supplied");
return false;
}
DigestAuthorizationRequestHeader header = parseHeader(request);
if (header == null)
{
log.error("Invalid Digest headers supplied:" + param);
return false;
}
String supportedQop = getQop(request, response);
boolean qopSupport = !Strings.isEmpty(supportedQop);
if (qopSupport)
{
// if we sent qop header the client must return one of the options
String[] qopOptions = supportedQop.split(" ");
boolean supported = false;
for (int i = 0; i < qopOptions.length && !supported; i++)
supported = qopOptions[i].equals(header.getQop());
if (!supported)
{
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
// we requested qop so these headers must be present
if (Strings.isEmpty(header.getCnonce()) || Strings.isEmpty(header.getNc()))
{
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
this.nonceCount++;
if (!validateNonceCount(header.getNonce(), header.getNc()))
{
log.warn("Nonce-count failed, expected: " + Integer.toHexString(this.nonceCount)
+ " but got " + header.getNc() + ", possible replay");
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
}
else
{
// no qop support so these headers are not allowed
if (!(Strings.isEmpty(header.getCnonce()) && Strings.isEmpty(header.getNc())))
{
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false;
}
}
// verify response (request-digest)
String expectedDigest;
// FIXME expected digest does not match digest generated by ie7, verify
// our specs.
String hA1 = getA1Checksum(header, request), a2 = getA2Value(header, request);
if ("auth".equals(header.getQop()) || "auth-int".equals(header.getQop()))
{
expectedDigest =
digest(header.getAlgorithm(), hA1, header.getNonce() + ":" + header.getNc() + ":"
+ header.getCnonce() + ":" + header.getQop() + ":"
+ checksum(header.getAlgorithm(), a2));
}
else
{
expectedDigest =
digest(header.getAlgorithm(), hA1, header.getNonce() + ":"
+ checksum(header.getAlgorithm(), a2));
}
if (!expectedDigest.equals(header.getResponse()))
{
log
.warn("A request-digest from the client did not match the expected value, this might indicate malicious activity.");
if (log.isDebugEnabled())
log.debug("Expected the following digest: \"" + expectedDigest
+ "\", the request contained the following headers: " + header);
}
else
{
// username and password are now available and validated
Object loginContext = getDigestLoginContext(header.getUsername());
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;
}
/**
* Delivers a context suitable for logging in with the specified username. The
* password can be considered verified, and is unknown to the server at this time
* (only a checksum is required to verify the password). Please refer to your specific
* wasp implementation for a suitable context.
*
* @param username
* @return the login context or null if none could be created
*/
protected abstract Object getDigestLoginContext(String username);
/**
* Calculates the checksum for the A1 value from the specification. This
* implementation knows how to calculate this value for MD5 and MD5-sess but requires
* the cleartext password. If you do not have the cleartext password you must know the
* checksum and return it here directly.
*
* @param header
* @param request
* @return the calculated value
* @throws WicketRuntimeException
* if the algorithm is not understood
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.2.2">section 3.2.2.2
* (A1)</a>
* @see #onMD5SessHashCalculated()
*/
protected String getA1Checksum(DigestAuthorizationRequestHeader header, WebRequest request)
{
if (Strings.isEmpty(header.getAlgorithm()) || "MD5".equals(header.getAlgorithm()))
return checksum(header.getAlgorithm(), header.getUsername() + ":" + header.getRealm()
+ ":" + getClearTextPassword(header.getUsername()));
else if ("MD5-sess".equals(header.getAlgorithm()))
{
if (Strings.isEmpty(getA1ChecksumForMD5Sess()))
{
setA1ChecksumForMD5Sess(checksum(header.getAlgorithm(), checksum(header
.getAlgorithm(), header.getUsername() + ":" + header.getRealm() + ":"
+ getClearTextPassword(header.getUsername()))
+ ":" + header.getNonce() + ":" + header.getCnonce()));
onMD5SessHashCalculated();
}
return getA1ChecksumForMD5Sess();
}
throw new WicketRuntimeException("Unable to handle algorithm:" + header.getAlgorithm());
}
/**
* Called when the MD5-sess algorithm is used and the hash is calculated for the first
* time. Implementations should use this notification to store this hash somewhere,
* like the session. Implementations are also responsible for retrieving this value
* uppon creation of this page.
*
* @see #getA1ChecksumForMD5Sess()
* @see #setA1ChecksumForMD5Sess(String)
*/
protected abstract void onMD5SessHashCalculated();
/**
* Calculates the A2 value from the specification. This implementation only knows how
* to calculate this value when there is no qop present or when the qop is 'auth'.
*
* @param header
* @param request
* @return the calculated value
* @throws WicketRuntimeException
* if the qop value is not understood
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.2.3">section 3.2.2.3
* (A2)</a>
*/
protected String getA2Value(DigestAuthorizationRequestHeader header, WebRequest request)
{
if (Strings.isEmpty(header.getQop()) || "auth".equals(header.getQop()))
return request.getHttpServletRequest().getMethod() + ":" + header.getUri();
throw new WicketRuntimeException("Unable to handle qop:" + header.getQop());
}
/**
* In order to prove the validity of the request the server need to have access to the
* clear text value of the password. Note that you only need to provide the clear text
* password if you do not have access to the H(A1) value. If you do you should return
* that in
* {@link #getA1Checksum(org.apache.wicket.security.login.http.HttpDigestLoginPage.DigestAuthorizationRequestHeader, WebRequest)}
* instead.
*
* @param username
* the username from the request
* @return the password as typed by the user
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.2.2">section 3.2.2.2
* (A1)</a>
*/
protected abstract String getClearTextPassword(String username);
/**
* Performs a digest over a secret and some data as required by the algorithm. The
* default only supports MD5 and MD5-sess.
*
* @param algorithm
* @param secret
* @param data
* @return the digest or null if the algorithm is not supported
* @see #checksum(String, String)
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.1">section 3.2.1
* (algorithm)</a>
*/
protected String digest(String algorithm, String secret, String data)
{
if ("MD5".equals(algorithm) || "MD5-sess".equals(algorithm))
{
return checksum(algorithm, secret + ":" + data);
}
return null;
}
/**
* Performs a checksum operation over the data as required by the algorithm. The
* default only supports MD5 and MD5-sess.
*
* @param algorithm
* @param data
* @return a checksum or null if the algorithm is not supported
* @throws WicketRuntimeException
* if the algorithm could not be located
* @see <a href="http://tools.ietf.org/html/rfc2617#section-3.2.1">section 3.2.1
* (algorithm)</a>
*/
protected String checksum(String algorithm, String data)
{
if ("MD5".equals(algorithm) || "MD5-sess".equals(algorithm))
{
MessageDigest digest = null;
try
{
digest = MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e)
{
throw new WicketRuntimeException("Client requested " + algorithm
+ ", but the algorithm could not be located");
}
digest.update(data.getBytes());
return new String(digest.digest());
}
return null;
}
/**
* @see org.apache.wicket.security.login.http.HttpAuthenticationLoginPage#requestAuthentication(org.apache.wicket.protocol.http.WebRequest,
* org.apache.wicket.protocol.http.WebResponse)
*/
@Override
protected void requestAuthentication(WebRequest request, WebResponse response)
{
if (allowBasicAuthenication)
super.requestAuthentication(request, response);
else
response.getHttpServletResponse().setStatus(HttpServletResponse.SC_UNAUTHORIZED);
addDigestHeaders(request, response);
}
/**
* Adds a "WWW-Authenticate" header for digest authentication to the response.
*
* @param request
* @param response
*/
protected void addDigestHeaders(WebRequest request, WebResponse response)
{
AppendingStringBuffer buffer = new AppendingStringBuffer(150);
buffer.append("Digest realm=\"").append(getRealm(request, response)).append("\"");
String domain = getDomain(request, response);
if (domain != null)
buffer.append(", domain=\"").append(domain).append("\"");
buffer.append(", nonce=\"").append(getNonce(request, response)).append("\"");
buffer.append(", opaque=\"").append(getNonce(request, response)).append("\"");
DigestAuthorizationRequestHeader header = parseHeader(request);
if (header != null)
buffer.append(", stale=\"").append(isNonceStale(request, header.getNonce())).append(
"\"");
String algorithm = getAlgorithm(request, response);
if (algorithm != null)
buffer.append(", algorithm=\"").append(algorithm).append("\"");
String qop = getQop(request, response);
if (qop != null)
buffer.append(", qop=\"").append(qop).append("\"");
response.getHttpServletResponse().addHeader("WWW-Authenticate", buffer.toString());
}
/**
* The optional qop-options as specified in section 3.2.1 of RFC 2617. According to
* the spec 'auth' or 'auth-int' can be used or neither. Multiple options must be
* separated by a space. Default is to return only 'auth'.
*
* @param request
* @param response
* @return a string identifying the supported qop-options
*/
protected String getQop(WebRequest request, WebResponse response)
{
return "auth";
}
/**
* The optional algorithm as specified in section 3.2.1 of RFC 2617. Default is to
* return MD5.
*
* @param request
* @param response
* @return a string identifying the request algorithm
*/
protected String getAlgorithm(WebRequest request, WebResponse response)
{
return "MD5";
}
/**
* The optional (list of) domain(s) as specified in section 3.2.1 of RFC 2617. Default
* is to return null.
*
* @param request
* @param response
* @return an unquoted list of space separated URI's, or null if all URI's on this
* server apply.
*/
protected String getDomain(WebRequest request, WebResponse response)
{
return null;
}
/**
* Validates the contents of nonce send with the request. as specified in section
* 3.2.1 of RFC 2617. By default it does not enforce a time limit on nonces but does
* check for a valid timestamp, ETag and private key.
*
* @param request
*
* @param nonce
* @return true if the nonce is stale, false otherwise
* @see #getNonce(WebRequest, WebResponse)
*/
protected boolean isNonceStale(WebRequest request, String nonce)
{
String[] parts = new String(Base64.decodeBase64(nonce.getBytes())).split(":");
if (parts.length != 3)
return true;
long nonceTime = 0;
try
{
nonceTime = Long.parseLong(parts[0]);
}
catch (NumberFormatException e)
{
return true;
}
if (nonceTime < 0 || nonceTime > System.currentTimeMillis())
return true;
if (!Objects.equal(parts[1], request.getHttpServletRequest().getHeader("ETag")))
return true;
return !Objects.equal(getPrivateKey(), parts[2]);
}
/**
* Validates the value of the nonce count.
*
* @param nonce
* the nonce used by the client
* @param clientNonceCount
* the number of times the client used this nonce, counted in hex
* @return true if the client nonce count matches the server nonce count, false
* otherwise
*/
protected boolean validateNonceCount(String nonce, String clientNonceCount)
{
if (clientNonceCount == null || clientNonceCount.length() == 0)
return false;
// skip leading zero's
int index = 0;
while (clientNonceCount.charAt(index) == '0')
index++;
return Integer.toHexString(this.nonceCount).equals(clientNonceCount.substring(index));
}
/**
* The nonce as specified in section 3.2.1 of RFC 2617.
*
* @param request
* @param response
* @return a base64 encoded string
* @see #isNonceStale(WebRequest, String)
*/
protected String getNonce(WebRequest request, WebResponse response)
{
long time = System.currentTimeMillis();
return new String(Base64.encodeBase64(new AppendingStringBuffer(50).append(time)
.append(":").append(request.getHttpServletRequest().getHeader("ETag")).append(":")
.append(getPrivateKey()).toString().getBytes()));
}
/**
* A private server key used by the default implementation of
* {@link #getNonce(WebRequest, WebResponse)}
*
* @return a private server key.
*/
protected String getPrivateKey()
{
return "Wasp, to protect and serve 'The Queen'.";
}
/**
* The opaque as specified in section 3.2.1 of RFC 2617.
*
* @param request
* @param response
* @return a base64 encoded string
*/
protected String getOpaque(WebRequest request, WebResponse response)
{
return new String(Base64.encodeBase64("Wicket, tastes like honey.".getBytes()));
}
/**
* Tells if besides digest also basic authentication is supported. Default is true
*
* @return true if basic authentication is also supported, false otherwise
*/
public final boolean isAllowBasicAuthenication()
{
return allowBasicAuthenication;
}
/**
* Sets the flag to allow or disallow basic authentication.
*
* @param allowBasicAuthenication
* allowBasicAuthenication
*/
public final void setAllowBasicAuthenication(boolean allowBasicAuthenication)
{
this.allowBasicAuthenication = allowBasicAuthenication;
}
/**
* Gets a1ValueForMD5Sess.
*
* @return a1ValueForMD5Sess
*/
protected final String getA1ChecksumForMD5Sess()
{
return a1ChecksumForMD5Sess;
}
/**
* Sets a1ValueForMD5Sess.
*
* @param valueForMD5Sess
* a1ValueForMD5Sess
*/
protected final void setA1ChecksumForMD5Sess(String valueForMD5Sess)
{
a1ChecksumForMD5Sess = valueForMD5Sess;
}
/**
* Parses the authorization header for a digest scheme.
*
* @param request
* @return the header or null if the header was not available or for a different
* scheme
*/
protected final DigestAuthorizationRequestHeader parseHeader(WebRequest request)
{
String header = request.getHttpServletRequest().getHeader("Authorization");
if (header == null)
return null;
if (!header.startsWith("Digest "))
return null;
header = header.substring(7);
Matcher m = HEADER_FIELDS.matcher(header);
if (!m.matches())
return null;
DigestAuthorizationRequestHeader digestHeader = new DigestAuthorizationRequestHeader();
m.reset();
while (m.find())
{
String key = m.group(2);
String value = m.group(3);
if (Strings.isEmpty(value))
value = m.group(4);
if (!digestHeader.addKeyValuePair(key, value))
log.warn("Unknown header: " + key + ", skipping header.");
}
return digestHeader;
}
/**
* Simple pojo to hold all the parsed fields from the request header "Authorization".
*
* @author marrink
*/
protected static final class DigestAuthorizationRequestHeader
{
private static final PropertyResolverConverter converter =
new NoOpPropertyResolverConverter();
private String username;
private String realm;
private String nonce;
private String uri;
private String qop;
private String nc;
private String cnonce;
private String response;
private String opaque;
private String algorithm;
/**
* Constructor to be used when key value pairs are going to be added later.
*
* @see #addKeyValuePair(String, String)
*/
protected DigestAuthorizationRequestHeader()
{
}
/**
* Dynamically resolves a header to the correct field and sets it value.
*
* @param key
* @param value
* @return true, if the value was set, false if the value could not be set
*/
public boolean addKeyValuePair(String key, String value)
{
if (Strings.isEmpty(key))
return false;
try
{
PropertyResolver.setValue(key, this, value, converter);
}
catch (WicketRuntimeException e)
{
log.debug("Failed to set header: " + key, e);
return false;
}
return true;
}
/**
* Gets realm.
*
* @return realm
*/
public final String getRealm()
{
return realm;
}
/**
* Gets nonce.
*
* @return nonce
*/
public final String getNonce()
{
return nonce;
}
/**
* Gets opaque.
*
* @return opaque
*/
public final String getOpaque()
{
return opaque;
}
/**
* Gets username.
*
* @return username
*/
public String getUsername()
{
return username;
}
/**
* Sets username.
*
* @param username
* username
*/
public void setUsername(String username)
{
this.username = username;
}
/**
* Gets uri.
*
* @return uri
*/
public String getUri()
{
return uri;
}
/**
* Sets uri.
*
* @param uri
* uri
*/
public void setUri(String uri)
{
this.uri = uri;
}
/**
* Gets qop.
*
* @return qop
*/
public String getQop()
{
return qop;
}
/**
* Sets qop.
*
* @param qop
* qop
*/
public void setQop(String qop)
{
this.qop = qop;
}
/**
* Gets nc.
*
* @return nc
*/
public String getNc()
{
return nc;
}
/**
* Sets nc.
*
* @param nc
* nc
*/
public void setNc(String nc)
{
this.nc = nc;
}
/**
* Gets cnonce.
*
* @return cnonce
*/
public String getCnonce()
{
return cnonce;
}
/**
* Sets cnonce.
*
* @param cnonce
* cnonce
*/
public void setCnonce(String cnonce)
{
this.cnonce = cnonce;
}
/**
* Gets response. aka request-digest.
*
* @return response
*/
public String getResponse()
{
return response;
}
/**
* Sets response.
*
* @param response
* response
*/
public void setResponse(String response)
{
this.response = response;
}
/**
* Sets realm.
*
* @param realm
* realm
*/
public void setRealm(String realm)
{
this.realm = realm;
}
/**
* Sets nonce.
*
* @param nonce
* nonce
*/
public void setNonce(String nonce)
{
this.nonce = nonce;
}
/**
* Sets opaque.
*
* @param opaque
* opaque
*/
public void setOpaque(String opaque)
{
this.opaque = opaque;
}
/**
* Gets algorithm.
*
* @return algorithm
*/
public String getAlgorithm()
{
return algorithm;
}
/**
* Sets algorithm.
*
* @param algorithm
* algorithm
*/
public void setAlgorithm(String algorithm)
{
this.algorithm = algorithm;
}
/**
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
AppendingStringBuffer buffer = new AppendingStringBuffer(150);
buffer.append("WWW-Authenticate: Digest username=\"").append(getUsername()).append(
"\", realm=\"").append(getRealm()).append("\", nonce=\"").append(getNonce())
.append("\", uri=\"").append(getUri()).append("\", qop=").append(getQop()).append(
", cnonce=\"").append(getCnonce()).append("\", nc=").append(getNc()).append(
", response=\"").append(getResponse()).append("\"");
return buffer.toString();
}
}
private static class NoOpPropertyResolverConverter extends PropertyResolverConverter
{
private static final long serialVersionUID = 1L;
/**
* Construct.
*/
public NoOpPropertyResolverConverter()
{
super(null, null);
}
/**
* @see org.apache.wicket.util.lang.PropertyResolverConverter#convert(java.lang.Object,
* java.lang.Class)
*/
@Override
public Object convert(Object object, Class< ? > clz)
{
return object; // assume correct type.
}
}
}