io.milton.http.http11.auth.CookieAuthenticationHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of milton-server-ce Show documentation
Show all versions of milton-server-ce Show documentation
Milton Community Edition: Supports DAV level 1 and is available on Apache2 license
The newest version!
/*
*
* Copyright 2014 McEvoy Software Ltd.
*
* 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 io.milton.http.http11.auth;
import io.milton.common.Utils;
import io.milton.http.*;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.principal.DiscretePrincipal;
import io.milton.resource.Resource;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This wraps a bunch of other authentication handlers, so if any of those
* successfully login the user then this will generate a cookie which can be
* used subsequently.
*
* Note that it is usually not correct to wrap a Digest auth handler because
* that would then defeat the purpose of digest authentication. However, this
* can and should wrap Basic and Form authentication handlers
*
* @author brad
*/
public class CookieAuthenticationHandler implements AuthenticationHandler {
private static final Logger log = LoggerFactory.getLogger(CookieAuthenticationHandler.class);
private static final String HANDLER_ATT_NAME = "_delegatedAuthenticationHandler";
public static final int SECONDS_PER_YEAR = 60 * 60 * 24 * 365;
private final String requestParamLogout = "miltonLogout";
private final String cookieUserUrlValue = "miltonUserUrl";
private final String cookieUserUrlHash = "miltonUserUrlHash";
private final String loginTokenName = "loginToken";
private final List handlers;
private final ResourceFactory principalResourceFactory;
private final NonceProvider nonceProvider;
private String userUrlAttName = "userUrl";
private boolean useLongLivedCookies = true;
private final List keys;
private final String keepLoggedInParamName = "keepLoggedIn";
public CookieAuthenticationHandler(NonceProvider nonceProvider, List handlers, ResourceFactory principalResourceFactory, List keys) {
this.nonceProvider = nonceProvider;
this.handlers = handlers;
this.principalResourceFactory = principalResourceFactory;
this.keys = keys;
}
@Override
public boolean credentialsPresent(Request request) {
String userUrl = getUserUrlFromRequest(request);
if (userUrl != null && userUrl.length() > 0) {
return true;
}
for (AuthenticationHandler h : handlers) {
if (h.credentialsPresent(request)) {
return true;
}
}
return false;
}
@Override
public boolean supports(Resource r, Request request) {
// find the authId, if any, from the request
// check for a logout command, if so logout
if (isLogout(request)) {
String userUrl = getUserUrl(request);
log.info("Is LogOut request, clear cookie");
if (userUrl != null && userUrl.length() > 0) {
clearCookieValue(HttpManager.response());
}
}
List supportingHandlers = new ArrayList<>();
for (AuthenticationHandler hnd : handlers) {
if (hnd.supports(r, request)) {
log.info("Found child handler who supports this request {}", hnd);
supportingHandlers.add(hnd);
}
}
if (!supportingHandlers.isEmpty()) {
request.getAttributes().put(HANDLER_ATT_NAME, supportingHandlers);
return true;
}
String userUrl = getUserUrl(request);
return userUrl != null;
}
@Override
public Object authenticate(Resource resource, Request request) {
// If there is a delegating handler which supports the request then we MUST use it
// This would have been selected in the supports method
List supportingHandlers = (List) request.getAttributes().get(HANDLER_ATT_NAME);
if (supportingHandlers != null && !supportingHandlers.isEmpty()) {
DiscretePrincipal lastUser = null;
for (AuthenticationHandler delegateHandler : supportingHandlers) {
if (log.isTraceEnabled()) {
log.trace("authenticate: use delegateHandler: " + delegateHandler);
}
Object tag = delegateHandler.authenticate(resource, request);
if (tag != null) {
if (tag instanceof DiscretePrincipal) {
lastUser = (DiscretePrincipal) tag;
setLoginCookies(lastUser, request);
log.trace("authenticate: authentication passed by delegated handler, persisted userUrl to cookie");
} else {
log.warn("authenticate: auth.tag is not an instance of " + DiscretePrincipal.class + ", is: " + tag.getClass() + " so is not compatible with cookie authentication");
// If form auth returned a non principal object then there is no way to
// persist the authentication state, so subsequent requests will fail. To prevent
// this we disable form auth and reject the login, this will result in a Basic/Digest
// authentication challenge
if (delegateHandler instanceof FormAuthenticationHandler) {
LoginResponseHandler.setDisableHtmlResponse(request);
return null;
}
}
return tag;
} else {
log.info("Login failed by delegated handler: " + delegateHandler.getClass());
// return null;
}
}
return lastUser;
} else {
log.trace("no delegating handler");
// No delegating handler means that we expect either to get a previous login token
// via a cookie, or this is an anonymous request
if (isLogout(request)) {
log.trace("authenticate: is logout");
return null;
} else {
String userUrl = getUserUrl(request);
if (userUrl == null) {
log.trace("authenticate: no userUrl in request or cookie, nothing to do");
// no token in request, so is anonymous
return null;
} else {
if (log.isTraceEnabled()) {
log.trace("authenticate: userUrl=" + userUrl);
}
// we found a userUrl
String host = request.getHostHeader();
Resource r;
try {
r = principalResourceFactory.getResource(host, userUrl);
log.trace("found current user: " + r);
} catch (NotAuthorizedException | BadRequestException ex) {
log.error("Couldnt check userUrl in cookie", ex);
r = null;
}
if (r == null) {
log.warn("User not found host: " + host + " userUrl: " + userUrl + " with resourcefactory: " + principalResourceFactory);
clearCookieValue(HttpManager.response());
} else {
// Logged in ok with details. Check if details came from request parameter, in
// which case we need to set cookies
if (request.getParams() != null && (request.getParams().containsKey(cookieUserUrlValue) || request.getParams().containsKey(loginTokenName))) {
if (r instanceof DiscretePrincipal) {
DiscretePrincipal dp = (DiscretePrincipal) r;
setLoginCookies(dp, request);
} else {
log.warn("Found user from request, but user object is not expected type. Should be " + DiscretePrincipal.class + " but is " + r.getClass());
}
} else {
log.trace("Do not set cookies, because token did not come from request variable");
}
}
return r;
}
}
}
}
/**
* Sets cookies to make the given user the currently logged in user for any
* subsequent requests.
*
* And also makes that user the current on-behalf-of user in
* CurrentUserService
*
* @param user
* @param request
*/
public void setLoginCookies(DiscretePrincipal user, Request request) {
log.trace("setLoginCookies");
if (user == null) {
throw new NullPointerException("user object is null");
}
if (user.getIdenitifer() == null) {
throw new NullPointerException("getIdenitifer object is null");
}
String userUrl = user.getIdenitifer().getValue();
if (userUrl == null) {
throw new NullPointerException("user identifier returned a null value");
}
setLoginCookies(userUrl, request);
}
public void setLoginCookies(String userUrl, Request request) {
if (request == null) {
return;
}
Response response = HttpManager.response();
if (response == null) {
log.trace("setLoginCookies: No response object");
return;
}
String signing = getUrlSigningHash(userUrl, request);
String sKeepLoggedIn = null;
if (request.getParams() != null) {
sKeepLoggedIn = request.getParams().get(keepLoggedInParamName);
}
boolean keepLoggedIn;
if (sKeepLoggedIn != null) {
keepLoggedIn = sKeepLoggedIn.equalsIgnoreCase("true");
} else {
keepLoggedIn = true; // default
}
setCookieValues(response, userUrl, signing, keepLoggedIn);
request.getAttributes().put(userUrlAttName, userUrl);
}
@Override
public void appendChallenges(Resource resource, Request request, List challenges) {
for (AuthenticationHandler h : handlers) {
if (h.isCompatible(resource, request)) {
h.appendChallenges(resource, request, challenges);
}
}
}
@Override
public boolean isCompatible(Resource resource, Request request) {
for (AuthenticationHandler h : handlers) {
if (h.isCompatible(resource, request)) {
return true;
}
}
return false;
}
private boolean isLogout(Request request) {
if (request.getParams() == null) {
return false;
}
String logoutCommand = request.getParams().get(requestParamLogout);
return (logoutCommand != null && logoutCommand.length() > 0);
}
/**
* Find a previous login token in the request, and if present verify its
* authenticity via a signing cookie
*
* @param request
* @return
*/
public String getUserUrl(Request request) {
if (request == null) {
return null;
}
String userUrl = getUserUrlFromRequest(request);
if (userUrl != null) {
userUrl = userUrl.trim();
if (userUrl.length() > 0) {
if (verifyHash(userUrl, request)) {
return userUrl;
} else {
log.info("Invalid userUrl hash, possible attempted hacking attempt. userUrl=" + userUrl);
}
}
}
return null;
}
public String getUserUrlFromRequest(Request request) {
String encodedUserUrl = null;
String lt = getCookieOrParam(request, loginTokenName);
if (lt != null) {
byte[] raw = Base64.getDecoder().decode(lt);
String params = new String(raw);
if (params.contains("|")) {
String[] parts = params.split("\\|");
if (parts.length == 2) {
encodedUserUrl = parts[0];
request.getAttributes().put(cookieUserUrlHash, parts[1]);
} else {
log.warn("getUserUrlFromRequest: loginToken is invalid: {}", params);
}
} else {
log.warn("getUserUrlFromRequest: loginToken is invalid: {}", params);
}
}
if (encodedUserUrl == null) {
encodedUserUrl = getCookieOrParam(request, cookieUserUrlValue);
}
if (encodedUserUrl == null) {
log.trace("getUserUrlFromRequest: Null encodedUserUrl");
return null;
}
if (log.isDebugEnabled()) {
log.debug("getUserUrlFromRequest: Raw:" + encodedUserUrl);
}
if (!encodedUserUrl.startsWith("b64")) {
log.trace("Looks like a plain path, return as is");
return encodedUserUrl;
} else {
log.trace("Looks like a base64 encoded string");
encodedUserUrl = encodedUserUrl.substring(3);
}
encodedUserUrl = Utils.decodePath(encodedUserUrl);
if (log.isDebugEnabled()) {
log.debug("getUserUrlFromRequest: Percent decoded:" + encodedUserUrl);
}
byte[] arr = Base64.getDecoder().decode(encodedUserUrl);
if (arr == null) {
log.debug("Failed to decode encodedUserUrl, so maybe its not encoded, return as it is");
return encodedUserUrl; // its just not encoded
}
String s = new String(arr);
if (log.isDebugEnabled()) {
log.debug("getUserUrlFromRequest: Decoded user url:" + s);
}
return s;
}
public String getHashFromRequest(Request request) {
String signing = getParamVal(request, cookieUserUrlHash);
if (signing == null) {
// See if we already got the signing hash
if (request.getAttributes().containsKey(cookieUserUrlHash)) {
signing = (String) request.getAttributes().get(cookieUserUrlHash);
}
if (signing == null) {
String lt = getCookieOrParam(request, loginTokenName);
if (lt != null) {
byte[] raw = Base64.getDecoder().decode(lt);
String params = new String(raw);
if (params.contains("|")) {
String[] parts = params.split("\\|");
if (parts.length == 2) {
signing = parts[1];
} else {
log.warn("getHashFromRequest: loginToken is invalid: {}", params);
}
} else {
log.warn("getHashFromRequest: loginToken is invalid: {}", params);
}
}
}
}
if (signing == null) {
signing = getCookieOrParam(request, cookieUserUrlHash);
}
return signing;
}
private boolean verifyHash(String userUrl, Request request) {
String signing = getHashFromRequest(request);
if (signing == null) {
return false;
}
signing = signing.replace("\"", "");
signing = signing.trim();
if (signing.length() == 0) {
log.warn("cookie signature is not present in cookie: " + cookieUserUrlHash);
return false;
}
for (String key : keys) {
if (key != null && key.length() > 0) {
if (verifyHash(userUrl, key, signing, request)) {
return true;
}
}
}
return false;
}
private boolean verifyHash(String userUrl, String key, String signing, Request request) {
// split the signing into nonce and hmac
int pos = signing.indexOf(":");
if (pos < 1) {
log.warn("Invalid cookie signing format, no semi-colon: " + signing + " Should be in form - nonce:hmac");
return false;
}
String host = getDomain(request);
String nonce = signing.substring(0, pos);
String hmac = signing.substring(pos + 1);
String message = nonce + ":" + userUrl + ":" + host;
// Check that the hmac is a valid signature
String expectedHmac = HmacUtils.calcShaHash(message, key);
if (log.isTraceEnabled()) {
log.trace("Message:" + message);
log.trace("Key:" + key);
log.trace("Hash:" + expectedHmac);
log.trace("Given Signing:" + signing);
}
boolean ok = expectedHmac.equals(hmac);
if (!ok) {
if (log.isDebugEnabled()) {
log.debug("Cookie sig does not match expected. Given=" + hmac + " Expected=" + expectedHmac);
}
return false;
} else {
// signed ok, check to see if nonce is still valid
NonceProvider.NonceValidity val = nonceProvider.getNonceValidity(nonce, null, userUrl);
if (val == null) {
throw new RuntimeException("Unhandled nonce validity value");
} else {
switch (val) {
case OK:
return true;
case EXPIRED:
// Hopefully the nonce provider will have a time limit and only return expired
// for recently expired nonces. So we will accept these but replace with a refreshed nonce
log.warn("Nonce is valid, but expired. We will accept it but reset it");
setLoginCookies(userUrl, request);
return true;
case INVALID:
log.warn("Received an invalid nonce: " + nonce + " not found in provider: " + nonceProvider);
return false;
default:
throw new RuntimeException("Unhandled nonce validity value");
}
}
}
}
private String getDomain(Request request) {
String host = request.getHostHeader();
if (host.contains(":")) {
host = host.substring(0, host.indexOf(":"));
}
if (host == null) {
host = "nohost";
}
return host;
}
/**
* The hmac signs a message in the form nonce || userUrl, where the nonce is
* requested from the nonceProvider
*
* This method returns a signing token in the form nonce || hmac
*
* @param userUrl
* @param request
* @return
*/
public String getUrlSigningHash(String userUrl, Request request) {
String host = getDomain(request);
return getUrlSigningHash(userUrl, request, host);
}
public String getUrlSigningHash(String userUrl, Request request, String host) {
String nonce = nonceProvider.createNonce(request, userUrl);
String message = nonce + ":" + userUrl + ":" + host;
String key = keys.get(keys.size() - 1); // Use the last key for new cookies
String hash = HmacUtils.calcShaHash(message, key);
String signing = nonce + ":" + hash;
if (log.isTraceEnabled()) {
log.trace("Message:" + message);
log.trace("Key:" + key);
log.trace("Hash:" + hash);
log.trace("Signing:" + signing);
}
return signing;
}
public String getLoginToken(String userUrl, Request request) {
String host = getDomain(request);
return getLoginToken(userUrl, request, host);
}
public String getLoginToken(String userUrl, Request request, String host) {
String hash = getUrlSigningHash(userUrl, request, host);
return getLoginToken(userUrl, hash);
}
public String getLoginToken(String userUrl, String urlSigningHash) {
String s = userUrl + '|' + urlSigningHash;
return Base64.getEncoder().encodeToString(s.getBytes());
}
private void setCookieValues(Response response, String userUrl, String hash, boolean keepLoggedIn) {
log.trace("setCookieValues");
BeanCookie c = new BeanCookie(cookieUserUrlValue);
String encodedUserUrl = encodeUserUrl(userUrl);
c.setValue(encodedUserUrl);
c.setPath("/");
c.setVersion(1);
if (keepLoggedIn && useLongLivedCookies) {
c.setExpiry(SECONDS_PER_YEAR);
}
response.setCookie(c);
c = new BeanCookie(cookieUserUrlHash);
c.setValue("\"" + hash + "\"");
c.setHttpOnly(true); // http only so not accessible from JS. Helps prevent XSS attacks
c.setVersion(1);
c.setPath("/");
if (keepLoggedIn && useLongLivedCookies) {
c.setExpiry(SECONDS_PER_YEAR);
}
response.setCookie(c);
}
public String encodeUserUrl(String userUrl) {
String encodedUserUrl = Base64.getEncoder().encodeToString(userUrl.getBytes(StandardCharsets.UTF_8));
encodedUserUrl = Utils.percentEncode(encodedUserUrl); // base64 uses some chars illegal in cookies, eg equals
encodedUserUrl = "b64" + encodedUserUrl; // need to distinguish if base64 encoded or not
return (encodedUserUrl);
}
private void clearCookieValue(Response response) {
log.info("clearCookieValue");
response.setCookie(cookieUserUrlValue, "");
response.setCookie(cookieUserUrlHash, "");
}
private String getCookieOrParam(Request request, String name) {
if (request == null) {
return null;
}
if (request.getParams() != null) {
String v = request.getParams().get(name);
if (v != null) {
return v;
}
}
Cookie c = request.getCookie(name);
if (c != null) {
return c.getValue();
}
return null;
}
private String getParamVal(Request request, String name) {
if (request.getParams() != null) {
String v = request.getParams().get(name);
return v;
}
return null;
}
public String getCookieNameUserUrlHash() {
return cookieUserUrlHash;
}
public String getCookieNameUserUrl() {
return cookieUserUrlValue;
}
public String getUserUrlAttName() {
return userUrlAttName;
}
public String getLoginTokenName() {
return loginTokenName;
}
public void setUserUrlAttName(String userUrlAttName) {
this.userUrlAttName = userUrlAttName;
}
public void setUseLongLivedCookies(boolean useLongLivedCookies) {
this.useLongLivedCookies = useLongLivedCookies;
}
public boolean isUseLongLivedCookies() {
return useLongLivedCookies;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy