Java tutorial
/* * Portions Copyright (c) 2009-10 Zepheira LLC, Some Rights Reserved * Portions Copyright (c) 2010-11 Talis Inc, Some Rights Reserved * * 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 org.callimachusproject.auth; import static org.callimachusproject.util.PercentCodec.decode; import static org.callimachusproject.util.PercentCodec.encode; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpMessage; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; import org.callimachusproject.server.exceptions.BadGateway; import org.callimachusproject.server.exceptions.BadRequest; import org.openrdf.OpenRDFException; import org.openrdf.model.Resource; import org.openrdf.model.URI; import org.openrdf.repository.object.ObjectConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Validates HTTP digest authorization. */ public class CookieAuthenticationManager implements DetachedAuthenticationManager { private static final BasicStatusLine _204 = new BasicStatusLine(HttpVersion.HTTP_1_1, 204, "No Content"); private static final BasicStatusLine _303 = new BasicStatusLine(HttpVersion.HTTP_1_1, 303, "See Other"); private static final Pattern SID_SPLIT = Pattern.compile("^(.*?):(.*?):(.*?):(.*)$"); private static final int LOGIN_GRP = 1; private static final int HASH_GRP = 2; private static final int NONCE_GRP = 3; private static final int IRI_GRP = 4; private final Logger logger = LoggerFactory.getLogger(CookieAuthenticationManager.class); private final String protectedPath; private final List<String> domains; private final Set<String> userCookies = new LinkedHashSet<String>(); private final SecureRandom random = new SecureRandom(); private final String identifier; private final byte[] secret; private final String redirect_prefix; private final String fullname_prefix; private final String secureCookie; private final String sid; private final ParameterAuthReader reader; public CookieAuthenticationManager(String identifier, String redirect_uri, String fullname_prefix, String path, List<String> domains, RealmManager realms, ParameterAuthReader reader) throws OpenRDFException, IOException { assert domains != null; assert domains.size() > 0; this.domains = domains; Set<Integer> ports = new HashSet<Integer>(); for (String domain : domains) { int port = java.net.URI.create(domain).getPort(); ports.add(port); StringBuilder suffix = new StringBuilder(); if (port > 0) { suffix.append(port); } if (domain.startsWith("https")) { suffix.append('s'); } suffix.append('='); userCookies.add("username" + suffix); } assert reader != null; assert identifier != null; this.reader = reader; this.identifier = identifier; this.redirect_prefix = redirect_uri + "&return_to="; this.fullname_prefix = fullname_prefix; assert redirect_uri.contains("?"); boolean secureOnly = identifier.startsWith("https"); this.protectedPath = path; this.secureCookie = secureOnly ? ";Secure" : ""; String hex = Integer.toHexString(Math.abs(identifier.hashCode())); this.sid = "sid" + hex + "="; String string = realms.getRealm(identifier).getOriginSecret(); if (Base64.isBase64(string)) { this.secret = Base64.decodeBase64(string); } else { this.secret = string.getBytes(Charset.forName("UTF-8")); } } @Override public String toString() { return getIdentifier(); } @Override public String getIdentifier() { return identifier; } public boolean isProtected(String url) { for (String domain : domains) { if (url.startsWith(domain)) return true; } return false; } @Override public HttpResponse unauthorized(String method, Object resource, Map<String, String[]> request, HttpEntity body) throws IOException { String url = getRequestUrl(request); String[] via = request.get("via"); Collection<String> cookie = asList(request.get("cookie")); boolean loggedIn = isCookiePresent(sid, cookie); HttpResponse resp = getLoginResponse(loggedIn, method, url, via, body); if (!resp.containsHeader("Cache-Control")) { resp.setHeader("Cache-Control", "no-store"); } return resp; } @Override public HttpMessage authenticationInfo(String method, Object resource, Map<String, String[]> request, ObjectConnection con) throws OpenRDFException, IOException { // no authentication info return null; } @Override public String authenticateRequest(String method, Object resource, Map<String, String[]> request, ObjectConnection con) { try { Collection<String> cookies = asList(request.get("cookie")); Matcher cookie = getVerifiedCookie(method, sid, cookies, SID_SPLIT); if (cookie == null) return null; return cookie.group(IRI_GRP); } catch (BadRequest e) { throw e; } catch (Exception e) { logger.warn(e.toString(), e); return null; } } @Override public HttpResponse logout(Collection<String> tokens) { HttpResponse resp = getLogoutResponse(); for (String userCookie : userCookies) { String secure = userCookie.endsWith("s=") ? ";Secure" : ""; resp.addHeader("Set-Cookie", userCookie + ";Max-Age=0;Path=" + protectedPath + secure); } return resp; } public String[] getUsernameSetCookie(Collection<String> tokens, ObjectConnection con) { String username = getUserLogin(tokens, con); return getUsernameSetCookie(username); } public String getUserIdentifier(String method, Collection<String> tokens, ObjectConnection con) throws OpenRDFException, IOException { Matcher cookie = getParsedCookie(sid, tokens, SID_SPLIT); if (cookie == null) return null; return cookie.group(IRI_GRP); } public String getUserLogin(Collection<String> tokens, ObjectConnection con) { Matcher cookie = getParsedCookie(sid, tokens, SID_SPLIT); if (cookie == null) return null; return cookie.group(LOGIN_GRP); } @Override public void registered(Resource invitedUser, URI registeredUser, ObjectConnection con) throws OpenRDFException { // welcome } private HttpResponse getLoginResponse(boolean loggedIn, String method, String url, String[] via, HttpEntity body) throws IOException { String target = url; String parameters = null; if (target.startsWith(redirect_prefix)) { String uri = target; String query = ""; int idx = target.indexOf('&', redirect_prefix.length()); if (idx > 0) { uri = target.substring(0, idx); query = target.substring(idx + 1); } target = getValueAfter(target, redirect_prefix); parameters = reader.getParameters(method, uri, query, body); if (reader.isCanncelled(parameters)) throw new BadGateway("Could not login"); if (reader.isLoggingIn(parameters)) { if (verify(parameters, via)) return redirectReturnTo(target, parameters); throw new BadGateway("Invalid login"); } } String location = reader.getLoginPage(redirect_prefix + encode(target), loggedIn, parameters, via); BasicHttpResponse resp = new BasicHttpResponse(_303); resp.addHeader("Location", location); resp.addHeader("Set-Cookie", sid + ";Max-Age=0;HttpOnly;Path=" + protectedPath + secureCookie); return resp; } private String getValueAfter(String parameters, String token) { if (parameters == null) return null; int idx = parameters.indexOf(token); if (idx < 0) return null; int start = idx + token.length(); int end = parameters.indexOf('&', start); if (end < 0) { end = parameters.length(); } return decode(parameters.substring(start, end)); } private boolean verify(String parameters, String[] via) { if (parameters == null) return false; try { if (reader.isValidParameters(parameters, via)) { return true; } else { return false; } } catch (Exception io) { logger.warn(io.toString(), io); return false; } } private HttpResponse redirectReturnTo(String url, String parameters) throws IOException { if (url.startsWith(fullname_prefix)) { // if this is a registration URL, update the fullname String fullname = reader.getUserFullName(parameters); String param = "&fullname=" + encode(fullname); if (url.contains("&fullname=")) { url.replaceAll("\\&fullname=[^\\&]*", param); } else if (url.contains("#")) { url.replaceFirst("#", param + "#"); } else { url = url + param; } } String username = reader.getUserLogin(parameters); String iri = reader.getUserIdentifier(parameters); String userInfo = getSidCookieValue(username, iri); BasicHttpResponse resp = new BasicHttpResponse(_303); resp.addHeader("Location", url); String encoded = encode(userInfo).replace("%2F", "/").replace("%3A", ":"); resp.addHeader("Set-Cookie", sid + encoded + ";HttpOnly;Path=" + protectedPath + secureCookie); for (String cookie : getUsernameSetCookie(username)) { resp.addHeader("Set-Cookie", cookie); } return resp; } private String[] getUsernameSetCookie(String username) { if (username == null) return new String[0]; int i = 0; String[] result = new String[userCookies.size()]; for (String userCookie : userCookies) { String secure = userCookie.endsWith("s=") ? ";Secure" : ""; result[i++] = userCookie + encode(username) + ";Path=" + protectedPath + secure; } return result; } private String getSidCookieValue(String login, String iri) throws IOException { String nonce = nextNonce(); int hour = getTimeSlot(System.currentTimeMillis()); String hash = getPassword(hour, login, iri, nonce); return login + ":" + hash + ":" + nonce + ":" + iri; } private HttpResponse getLogoutResponse() { BasicHttpResponse resp = new BasicHttpResponse(_204); resp.addHeader("Set-Cookie", sid + ";Max-Age=0;HttpOnly;Path=" + protectedPath + secureCookie); return resp; } private Matcher getVerifiedCookie(String method, String name, Collection<String> cookies, Pattern pattern) throws IOException { Matcher m = getParsedCookie(name, cookies, pattern); if (m == null) return null; String nonce = m.group(NONCE_GRP); String hash = m.group(HASH_GRP); String username = m.group(LOGIN_GRP); String iri = m.group(IRI_GRP); int hour = getTimeSlot(System.currentTimeMillis()); int leeway = hour - 3; if (!"GET".equals(method)) { leeway -= 6; } for (int h = hour; h >= leeway; h--) { String password = getPassword(h, username, iri, nonce); if (hash.equals(password)) return m; } return m; } private Matcher getParsedCookie(String name, Collection<String> cookies, Pattern pattern) { String cookie = getCookie(name, cookies); if (cookie == null) return null; Matcher matcher = pattern.matcher(cookie); if (matcher.find()) return matcher; return null; } private String getCookie(String token, Collection<String> cookies) { if (cookies == null) return null; for (String cookie : cookies) { if (!cookie.contains(token)) continue; for (String p : cookie.split("\\s*;\\s*")) { if (p.startsWith(token)) { String raw = p.substring(token.length()); if (raw.length() == 0) return null; return decode(raw); } } } return null; } private boolean isCookiePresent(String name, Collection<String> cookies) { return getCookie(sid, cookies) != null; } private String nextNonce() { synchronized (random) { return Long.toString(Math.abs(random.nextLong()), Character.MAX_RADIX); } } private int getTimeSlot(long now) { long slot = now / 1000 / 60 / 5; return (int) slot; } private String getPassword(int hour, String username, String iri, String nonce) throws IOException { int size = secret.length + username.length() + iri.length() + nonce.length(); ByteArrayOutputStream baos = new ByteArrayOutputStream(size * 2); baos.write(secret); for (int i = 0, n = Integer.SIZE / Byte.SIZE; i < n; i++) { baos.write((byte) hour); hour >>= Byte.SIZE; } baos.write(getIdentifier().getBytes("UTF-8")); baos.write(username.getBytes("UTF-8")); baos.write(iri.getBytes("UTF-8")); baos.write(nonce.getBytes("UTF-8")); return new String(Hex.encodeHex(DigestUtils.md5(baos.toByteArray()))); } private String getRequestUrl(Map<String, String[]> request) { String target = request.get("request-target")[0]; if (target.charAt(0) != '/') return target; String scheme = request.get("request-scheme")[0]; String host = request.get("host")[0]; return scheme + "://" + host + target; } private Collection<String> asList(String[] array) { if (array == null) return null; return Arrays.asList(array); } }