/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.crypto;
import java.security.GeneralSecurityException;
import java.util.logging.Level;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Cookie;
import org.restlet.data.CookieSetting;
import org.restlet.data.Form;
import org.restlet.data.Method;
import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.engine.util.Base64;
import org.restlet.ext.crypto.internal.CryptoUtils;
import org.restlet.security.ChallengeAuthenticator;
/**
* Challenge authenticator based on browser cookies. This is useful when the web
* application requires a finer grained control on the login and logout process
* and can't rely solely on standard schemes such as
* {@link ChallengeScheme#HTTP_BASIC}.<br>
* <br>
* Login can be automatically handled by intercepting HTTP POST calls to the
* {@link #getLoginPath()} URI. The request entity should contain an HTML form
* with two fields, the first one named {@link #getIdentifierFormName()} and the
* second one named {@link #getSecretFormName()}.<br>
* <br>
* Logout can be automatically handled as well by intercepting HTTP GET or POST
* calls to the {@link #getLogoutPath()} URI.<br>
* <br>
* After login or logout, the user's browser can be redirected to the URI
* provided in a query parameter named by {@link #getRedirectQueryName()}.<br>
* <br>
* When the credentials are missing or stale, the
* {@link #challenge(Response, boolean)} method is invoked by the parent class,
* and its default behavior is to redirect the user's browser to the
* {@link #getLoginFormPath()} URI, adding the URI of the target resource as a
* query parameter of name {@link #getRedirectQueryName()}.<br>
* <br>
* Note that credentials, both identifier and secret, are stored in a cookie in
* an encrypted manner. The default encryption algorithm is AES but can be
* changed with {@link #setEncryptAlgorithm(String)}. It is also strongly
* recommended to
*
* @author Remi Dewitte
* @author Jerome Louvel
*/
public class CookieAuthenticator extends ChallengeAuthenticator {
/** The name of the cookie that stores log info. */
private volatile String cookieName;
/** The name of the algorithm used to encrypt the log info cookie value. */
private volatile String encryptAlgorithm;
/**
* The secret key for the algorithm used to encrypt the log info cookie
* value.
*/
private volatile byte[] encryptSecretKey;
/** The name of the HTML login form field containing the identifier. */
private volatile String identifierFormName;
/** Indicates if the login requests should be intercepted. */
private volatile boolean interceptingLogin;
/** Indicates if the logout requests should be intercepted. */
private volatile boolean interceptingLogout;
/** The URI path of the HTML login form to use to challenge the user. */
private volatile String loginFormPath;
/** The login URI path to intercept. */
private volatile String loginPath;
/** The logout URI path to intercept. */
private volatile String logoutPath;
/** The maximum age of the log info cookie. */
private volatile int maxCookieAge;
/**
* The name of the query parameter containing the URI to redirect the
* browser to after login or logout.
*/
private volatile String redirectQueryName;
/** The name of the HTML login form field containing the secret. */
private volatile String secretFormName;
/**
* Constructor. Use the {@link ChallengeScheme#HTTP_COOKIE} pseudo-scheme.
*
* @param context
* The parent context.
* @param optional
* Indicates if this authenticator is optional so alternative
* authenticators down the chain can be attempted.
* @param realm
* The name of the security realm.
* @param encryptSecretKey
* The secret key used to encrypt the cookie value.
*/
public CookieAuthenticator(Context context, boolean optional, String realm,
byte[] encryptSecretKey) {
super(context, optional, ChallengeScheme.HTTP_COOKIE, realm);
this.cookieName = "Credentials";
this.interceptingLogin = true;
this.interceptingLogout = true;
this.identifierFormName = "login";
this.loginPath = "/login";
this.logoutPath = "/logout";
this.secretFormName = "password";
this.encryptAlgorithm = "AES";
this.encryptSecretKey = encryptSecretKey;
this.maxCookieAge = -1;
this.redirectQueryName = "targetUri";
}
/**
* Constructor for mandatory cookie authenticators.
*
* @param context
* The parent context.
* @param realm
* The name of the security realm.
* @param encryptSecretKey
* The secret key used to encrypt the cookie value.
*/
public CookieAuthenticator(Context context, String realm,
byte[] encryptSecretKey) {
this(context, false, realm, encryptSecretKey);
}
/**
* Attempts to redirect the user's browser to the URI provided in a query
* parameter named by {@link #getRedirectQueryName()}.
*
* @param request
* The current request.
* @param response
* The current response.
*/
protected void attemptRedirect(Request request, Response response) {
String targetUri = request.getResourceRef().getQueryAsForm()
.getFirstValue(getRedirectQueryName());
if (targetUri != null) {
response.redirectSeeOther(Reference.decode(targetUri));
}
}
/**
* Restores credentials from the cookie named {@link #getCookieName()} if
* available. The usual processing is the followed.
*/
@Override
protected boolean authenticate(Request request, Response response) {
// Restore credentials from the cookie
Cookie credentialsCookie = request.getCookies().getFirst(
getCookieName());
if (credentialsCookie != null) {
request.setChallengeResponse(parseCredentials(credentialsCookie
.getValue()));
}
return super.authenticate(request, response);
}
/**
* Sets or updates the credentials cookie.
*/
@Override
protected int authenticated(Request request, Response response) {
try {
CookieSetting credentialsCookie = getCredentialsCookie(request,
response);
credentialsCookie.setValue(formatCredentials(request
.getChallengeResponse()));
credentialsCookie.setMaxAge(getMaxCookieAge());
} catch (GeneralSecurityException e) {
getLogger().log(Level.SEVERE,
"Could not format credentials cookie", e);
}
return super.authenticated(request, response);
}
/**
* Optionally handles the login and logout actions by intercepting the HTTP
* calls to the {@link #getLoginPath()} and {@link #getLogoutPath()} URIs.
*/
@Override
protected int beforeHandle(Request request, Response response) {
if (isLoggingIn(request, response)) {
login(request, response);
} else if (isLoggingOut(request, response)) {
return logout(request, response);
}
return super.beforeHandle(request, response);
}
/**
* This method should be overridden to return a login form representation.<br>
* By default, it redirects the user's browser to the
* {@link #getLoginFormPath()} URI, adding the URI of the target resource as
* a query parameter of name {@link #getRedirectQueryName()}.<br>
* In case the getLoginFormPath() is not set, it calls the parent's method.
*/
@Override
public void challenge(Response response, boolean stale) {
if (getLoginFormPath() == null) {
super.challenge(response, stale);
} else {
Reference ref = response.getRequest().getResourceRef();
String redirectQueryName = getRedirectQueryName();
String redirectQueryValue = ref.getQueryAsForm().getFirstValue(
redirectQueryName, "");
if ("".equals(redirectQueryValue)) {
redirectQueryValue = new Reference(getLoginFormPath())
.addQueryParameter(redirectQueryName, ref.toString())
.toString();
}
response.redirectSeeOther(redirectQueryValue);
}
}
/**
* Formats the raws credentials to store in the cookie.
*
* @param challenge
* The challenge response to format.
* @return The raw credentials.
* @throws GeneralSecurityException
*/
protected String formatCredentials(ChallengeResponse challenge)
throws GeneralSecurityException {
// Data buffer
StringBuffer sb = new StringBuffer();
// Indexes buffer
StringBuffer isb = new StringBuffer();
String timeIssued = Long.toString(System.currentTimeMillis());
int i = timeIssued.length();
sb.append(timeIssued);
isb.append(i);
String identifier = challenge.getIdentifier();
sb.append('/');
sb.append(identifier);
i += identifier.length() + 1;
isb.append(',').append(i);
sb.append('/');
sb.append(challenge.getSecret());
// Store indexes at the end of the string
sb.append('/');
sb.append(isb);
return Base64.encode(CryptoUtils.encrypt(getEncryptAlgorithm(),
getEncryptSecretKey(), sb.toString()), false);
}
/**
* Returns the cookie name to use for the authentication credentials. By
* default, it is is "Credentials".
*
* @return The cookie name to use for the authentication credentials.
*/
public String getCookieName() {
return cookieName;
}
/**
* Returns the credentials cookie setting. It first try to find an existing
* cookie. If necessary, it creates a new one.
*
* @param request
* The current request.
* @param response
* The current response.
* @return The credentials cookie setting.
*/
protected CookieSetting getCredentialsCookie(Request request,
Response response) {
CookieSetting credentialsCookie = response.getCookieSettings()
.getFirst(getCookieName());
if (credentialsCookie == null) {
credentialsCookie = new CookieSetting(getCookieName(), null);
credentialsCookie.setAccessRestricted(true);
// authCookie.setVersion(1);
if (request.getRootRef() != null) {
String p = request.getRootRef().getPath();
credentialsCookie.setPath(p == null ? "/" : p);
} else {
// authCookie.setPath("/");
}
response.getCookieSettings().add(credentialsCookie);
}
return credentialsCookie;
}
/**
* Returns the name of the algorithm used to encrypt the log info cookie
* value. By default, it returns "AES".
*
* @return The name of the algorithm used to encrypt the log info cookie
* value.
*/
public String getEncryptAlgorithm() {
return encryptAlgorithm;
}
/**
* Returns the secret key for the algorithm used to encrypt the log info
* cookie value.
*
* @return The secret key for the algorithm used to encrypt the log info
* cookie value.
*/
public byte[] getEncryptSecretKey() {
return encryptSecretKey;
}
/**
* Returns the name of the HTML login form field containing the identifier.
* Returns "login" by default.
*
* @return The name of the HTML login form field containing the identifier.
*/
public String getIdentifierFormName() {
return identifierFormName;
}
/**
* Returns the URI path of the HTML login form to use to challenge the user.
*
* @return The URI path of the HTML login form to use to challenge the user.
*/
public String getLoginFormPath() {
return loginFormPath;
}
/**
* Returns the login URI path to intercept.
*
* @return The login URI path to intercept.
*/
public String getLoginPath() {
return loginPath;
}
/**
* Returns the logout URI path to intercept.
*
* @return The logout URI path to intercept.
*/
public String getLogoutPath() {
return logoutPath;
}
/**
* Returns the maximum age of the log info cookie. By default, it uses -1 to
* make the cookie only last until the end of the current browser session.
*
* @return The maximum age of the log info cookie.
* @see CookieSetting#getMaxAge()
*/
public int getMaxCookieAge() {
return maxCookieAge;
}
/**
* Returns the name of the query parameter containing the URI to redirect
* the browser to after login or logout. By default, it uses "targetUri".
*
* @return The name of the query parameter containing the URI to redirect
* the browser to after login or logout.
*/
public String getRedirectQueryName() {
return redirectQueryName;
}
/**
* Returns the name of the HTML login form field containing the secret.
* Returns "password" by default.
*
* @return The name of the HTML login form field containing the secret.
*/
public String getSecretFormName() {
return secretFormName;
}
/**
* Indicates if the login requests should be intercepted.
*
* @return True if the login requests should be intercepted.
*/
public boolean isInterceptingLogin() {
return interceptingLogin;
}
/**
* Indicates if the logout requests should be intercepted.
*
* @return True if the logout requests should be intercepted.
*/
public boolean isInterceptingLogout() {
return interceptingLogout;
}
/**
* Indicates if the request is an attempt to log in and should be
* intercepted.
*
* @param request
* The current request.
* @param response
* The current response.
* @return True if the request is an attempt to log in and should be
* intercepted.
*/
protected boolean isLoggingIn(Request request, Response response) {
return isInterceptingLogin()
&& getLoginPath()
.equals(request.getResourceRef().getRemainingPart(
false, false))
&& Method.POST.equals(request.getMethod());
}
/**
* Indicates if the request is an attempt to log out and should be
* intercepted.
*
* @param request
* The current request.
* @param response
* The current response.
* @return True if the request is an attempt to log out and should be
* intercepted.
*/
protected boolean isLoggingOut(Request request, Response response) {
return isInterceptingLogout()
&& getLogoutPath()
.equals(request.getResourceRef().getRemainingPart(
false, false))
&& (Method.GET.equals(request.getMethod()) || Method.POST
.equals(request.getMethod()));
}
/**
* Processes the login request.
*
* @param request
* The current request.
* @param response
* The current response.
*/
protected void login(Request request, Response response) {
// Login detected
Form form = new Form(request.getEntity());
Parameter identifier = form.getFirst(getIdentifierFormName());
Parameter secret = form.getFirst(getSecretFormName());
// Set credentials
ChallengeResponse cr = new ChallengeResponse(getScheme(),
identifier != null ? identifier.getValue() : null,
secret != null ? secret.getValue() : null);
request.setChallengeResponse(cr);
// Attempt to redirect
attemptRedirect(request, response);
}
/**
* Processes the logout request.
*
* @param request
* The current request.
* @param response
* The current response.
*/
protected int logout(Request request, Response response) {
// Clears the credentials
request.setChallengeResponse(null);
CookieSetting credentialsCookie = getCredentialsCookie(request,
response);
credentialsCookie.setMaxAge(0);
// Attempt to redirect
attemptRedirect(request, response);
return STOP;
}
/**
* Decodes the credentials stored in a cookie into a proper
* {@link ChallengeResponse} object.
*
* @param cookieValue
* The credentials to decode from cookie value.
* @return The credentials as a proper challenge response.
*/
protected ChallengeResponse parseCredentials(String cookieValue) {
// 1) Decode Base64 string
byte[] encrypted = Base64.decode(cookieValue);
if (encrypted == null) {
getLogger().warning(
"Cannot decode cookie credentials : " + cookieValue);
}
// 2) Decrypt the credentials
try {
String decrypted = CryptoUtils.decrypt(getEncryptAlgorithm(),
getEncryptSecretKey(), encrypted);
// 3) Parse the decrypted cookie value
int lastSlash = decrypted.lastIndexOf('/');
String[] indexes = decrypted.substring(lastSlash + 1).split(",");
int identifierIndex = Integer.parseInt(indexes[0]);
int secretIndex = Integer.parseInt(indexes[1]);
// 4) Create the challenge response
ChallengeResponse cr = new ChallengeResponse(getScheme());
cr.setRawValue(cookieValue);
cr.setTimeIssued(Long.parseLong(decrypted.substring(0,
identifierIndex)));
cr.setIdentifier(decrypted.substring(identifierIndex + 1,
secretIndex));
cr.setSecret(decrypted.substring(secretIndex + 1, lastSlash));
return cr;
} catch (Exception e) {
getLogger().log(Level.INFO, "Unable to decrypt cookie credentials",
e);
return null;
}
}
/**
* Sets the cookie name to use for the authentication credentials.
*
* @param cookieName
* The cookie name to use for the authentication credentials.
*/
public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
/**
* Sets the name of the algorithm used to encrypt the log info cookie value.
*
* @param secretAlgorithm
* The name of the algorithm used to encrypt the log info cookie
* value.
*/
public void setEncryptAlgorithm(String secretAlgorithm) {
this.encryptAlgorithm = secretAlgorithm;
}
/**
* Sets the secret key for the algorithm used to encrypt the log info cookie
* value.
*
* @param secretKey
* The secret key for the algorithm used to encrypt the log info
* cookie value.
*/
public void setEncryptSecretKey(byte[] secretKey) {
this.encryptSecretKey = secretKey;
}
/**
* Sets the name of the HTML login form field containing the identifier.
*
* @param loginInputName
* The name of the HTML login form field containing the
* identifier.
*/
public void setIdentifierFormName(String loginInputName) {
this.identifierFormName = loginInputName;
}
/**
* Indicates if the login requests should be intercepted.
*
* @param intercepting
* True if the login requests should be intercepted.
*/
public void setInterceptingLogin(boolean intercepting) {
this.interceptingLogin = intercepting;
}
/**
* Indicates if the logout requests should be intercepted.
*
* @param intercepting
* True if the logout requests should be intercepted.
*/
public void setInterceptingLogout(boolean intercepting) {
this.interceptingLogout = intercepting;
}
/**
* Sets the URI path of the HTML login form to use to challenge the user.
*
* @param loginFormPath
* The URI path of the HTML login form to use to challenge the
* user.
*/
public void setLoginFormPath(String loginFormPath) {
this.loginFormPath = loginFormPath;
}
/**
* Sets the login URI path to intercept.
*
* @param loginPath
* The login URI path to intercept.
*/
public void setLoginPath(String loginPath) {
this.loginPath = loginPath;
}
/**
* Sets the logout URI path to intercept.
*
* @param logoutPath
* The logout URI path to intercept.
*/
public void setLogoutPath(String logoutPath) {
this.logoutPath = logoutPath;
}
/**
* Sets the maximum age of the log info cookie.
*
* @param timeout
* The maximum age of the log info cookie.
* @see CookieSetting#setMaxAge(int)
*/
public void setMaxCookieAge(int timeout) {
this.maxCookieAge = timeout;
}
/**
* Sets the name of the query parameter containing the URI to redirect the
* browser to after login or logout.
*
* @param redirectQueryName
* The name of the query parameter containing the URI to redirect
* the browser to after login or logout.
*/
public void setRedirectQueryName(String redirectQueryName) {
this.redirectQueryName = redirectQueryName;
}
/**
* Sets the name of the HTML login form field containing the secret.
*
* @param passwordInputName
* The name of the HTML login form field containing the secret.
*/
public void setSecretFormName(String passwordInputName) {
this.secretFormName = passwordInputName;
}
}