/** * Most of the code in the Qalingo project is copyrighted Hoteia and licensed * under the Apache License Version 2.0 (release version 0.8.0) * http://www.apache.org/licenses/LICENSE-2.0 * * Copyright (c) Hoteia, 2012-2014 * http://www.hoteia.com - http://twitter.com/hoteia - contact@hoteia.com * */ package org.hoteia.qalingo.core.service.openid; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * */ @Service("openIdService") public class OpenIdService { private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1"; public static final String OPEN_ID_PROVIDER_ALIAS_SUFIX = ".alias"; @Autowired private Properties openIdProperties; private Map<String, Endpoint> endpointCache = new ConcurrentHashMap<String, Endpoint>(); private Map<Endpoint, Association> associationCache = new ConcurrentHashMap<Endpoint, Association>(); private Map<String, String> urlMap = new HashMap<String, String>(); private Map<String, String> aliasMap = new HashMap<String, String>(); private int timeOut = 5000; // 5 seconds private String assocQuery = null; private String authQuery = null; private String returnTo = null; private String returnToUrlEncode = null; private String realm = null; /** * Set returning address after authentication. * * @param returnTo URL that should redirect to. */ public void setReturnTo(String returnTo) { try { this.returnToUrlEncode = Utils.urlEncode(returnTo); } catch(UnsupportedEncodingException e) { throw new OpenIdException(e); } this.returnTo = returnTo; } /** * Set realm. For example, "http://*.example.com". * * @param realm Realm of RP. */ public void setRealm(String realm) { try { this.realm = Utils.urlEncode(realm); } catch(UnsupportedEncodingException e) { throw new OpenIdException(e); } } /** * Set timeout in milliseconds. */ public void setTimeOut(int timeOutInMilliseconds) { this.timeOut = timeOutInMilliseconds; } /** * Get authentication information from HTTP request, key.and alias */ public OpenIdAuthentication getAuthentication(HttpServletRequest request, byte[] key, String alias) { // verify: String identity = request.getParameter("openid.identity"); if (identity == null) throw new OpenIdException("Missing 'openid.identity'."); if (request.getParameter("openid.invalidate_handle")!=null) throw new OpenIdException("Invalidate handle."); String sig = request.getParameter("openid.sig"); if (sig == null) throw new OpenIdException("Missing 'openid.sig'."); String signed = request.getParameter("openid.signed"); if (signed == null) throw new OpenIdException("Missing 'openid.signed'."); if (!returnTo.equals(request.getParameter("openid.return_to"))) throw new OpenIdException("Bad 'openid.return_to'."); // check sig: String[] params = signed.split("[\\,]+"); StringBuilder sb = new StringBuilder(1024); for (String param : params) { sb.append(param) .append(':'); String value = request.getParameter("openid." + param); if (value!=null) sb.append(value); sb.append('\n'); } String hmac = getHmacSha1(sb.toString(), key); if (!safeEquals(sig, hmac)){ throw new OpenIdException("Verify signature failed."); } // set auth: OpenIdAuthentication auth = new OpenIdAuthentication(); auth.setIdentity(identity); auth.setEmail(request.getParameter("openid." + alias + ".value.email")); auth.setLanguage(request.getParameter("openid." + alias + ".value.language")); auth.setGender(request.getParameter("openid." + alias + ".value.gender")); auth.setFullname(getFullname(request, alias)); auth.setFirstname(getFirstname(request, alias)); auth.setLastname(getLastname(request, alias)); return auth; } boolean safeEquals(String s1, String s2) { if (s1.length()!=s2.length()){ return false; } int result = 0; for (int i=0; i<s1.length(); i++) { int c1 = s1.charAt(i); int c2 = s2.charAt(i); result |= (c1 ^c2); } return result==0; } String getLastname (HttpServletRequest request, String axa) { String name = request.getParameter("openid." + axa + ".value.lastname"); // If lastname is not supported try to get it from the fullname if (name == null) { name = request.getParameter("openid." + axa + ".value.fullname"); if (name != null) { int n = name.lastIndexOf(' '); if (n!=(-1)){ name = name.substring(n + 1); } } } return name; } String getFirstname(HttpServletRequest request, String axa) { String name = request.getParameter("openid." + axa + ".value.firstname"); //If firstname is not supported try to get it from the fullname if (name == null) { name = request.getParameter("openid." + axa + ".value.fullname"); if (name != null) { int n = name.indexOf(' '); if (n!=(-1)){ name = name.substring(0, n); } } } return name; } String getFullname(HttpServletRequest request, String axa) { // If fullname is not supported then get combined first and last name String fname = request.getParameter("openid."+axa+".value.fullname"); if (fname == null) { fname = request.getParameter("openid."+axa+".value.firstname"); if (fname != null) { fname += " "; } fname += request.getParameter("openid."+axa+".value.lastname"); } return fname; } String getHmacSha1(String data, byte[] key) { SecretKeySpec signingKey = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM); Mac mac = null; try { mac = Mac.getInstance(HMAC_SHA1_ALGORITHM); mac.init(signingKey); } catch(NoSuchAlgorithmException e) { throw new OpenIdException(e); } catch(InvalidKeyException e) { throw new OpenIdException(e); } try { byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8")); return Base64.encodeBytes(rawHmac); } catch(IllegalStateException e) { throw new OpenIdException(e); } catch(UnsupportedEncodingException e) { throw new OpenIdException(e); } } /** * Lookup end point by name or full URL. */ public Endpoint lookupEndpoint(String nameOrUrl) { String url = null; String alias = null; if (nameOrUrl.startsWith("http://") || nameOrUrl.startsWith("https://")){ url = nameOrUrl; } else { url = lookupUrlByName(nameOrUrl); if (url == null){ throw new OpenIdException("Cannot find OP URL by name: " + nameOrUrl); } alias = lookupAliasByName(nameOrUrl); } Endpoint endpoint = endpointCache.get(url); if (endpoint != null && !endpoint.isExpired()){ return endpoint; } endpoint = requestEndpoint(url, alias==null ? Endpoint.DEFAULT_ALIAS : alias); endpointCache.put(url, endpoint); return endpoint; } public Association lookupAssociation(Endpoint endpoint) { Association assoc = associationCache.get(endpoint); if (assoc != null && !assoc.isExpired()){ return assoc; } assoc = requestAssociation(endpoint); associationCache.put(endpoint, assoc); return assoc; } public String getAuthenticationUrl(Endpoint endpoint, Association association) { StringBuilder sb = new StringBuilder(1024); sb.append(endpoint.getUrl()) .append(endpoint.getUrl().contains("?") ? '&' : '?') .append(getAuthQuery(endpoint.getAlias())) .append("&openid.return_to=") .append(returnToUrlEncode) .append("&openid.assoc_handle=") .append(association.getAssociationHandle()); if (realm != null){ sb.append("&openid.realm=").append(realm); } return sb.toString(); } Endpoint requestEndpoint(String url, String alias) { Map<String, Object> map = Utils.httpRequest(url, "GET", "application/xrds+xml", null, timeOut); try { String content = Utils.getContent(map); return new Endpoint(Utils.mid(content, "<URI>", "</URI>"), alias, Utils.getMaxAge(map)); } catch(UnsupportedEncodingException e) { throw new OpenIdException(e); } } Association requestAssociation(Endpoint endpoint) { Map<String, Object> map = Utils.httpRequest(endpoint.getUrl(), "POST", "*/*", getAssocQuery(), timeOut); String content = null; try { content = Utils.getContent(map); } catch(UnsupportedEncodingException e) { throw new OpenIdException(e); } Association assoc = new Association(); try { BufferedReader r = new BufferedReader(new StringReader(content)); for (;;) { String line = r.readLine(); if (line == null){ break; } line = line.trim(); int pos = line.indexOf(':'); if (pos!=(-1)) { String key = line.substring(0, pos); String value = line.substring(pos + 1); if ("session_type".equals(key)) assoc.setSessionType(value); else if ("assoc_type".equals(key)) assoc.setAssociationType(value); else if ("assoc_handle".equals(key)) assoc.setAssociationHandle(value); else if ("mac_key".equals(key)) assoc.setMacKey(value); else if ("expires_in".equals(key)) { long maxAge = Long.parseLong(value); assoc.setMaxAge(maxAge * 900L); // 90% } } } } catch(IOException e) { throw new RuntimeException("Association is impossible!", e); } return assoc; } private String getAuthQuery(String axa) { if (authQuery != null){ return authQuery; } List<String> list = new ArrayList<String>(); list.add("openid.ns=http://specs.openid.net/auth/2.0"); list.add("openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select"); list.add("openid.identity=http://specs.openid.net/auth/2.0/identifier_select"); list.add("openid.mode=checkid_setup"); list.add("openid.ns." + axa + "=http://openid.net/srv/ax/1.0"); list.add("openid." + axa + ".mode=fetch_request"); list.add("openid." + axa + ".type.email=http://axschema.org/contact/email"); list.add("openid." + axa + ".type.fullname=http://axschema.org/namePerson"); list.add("openid." + axa + ".type.language=http://axschema.org/pref/language"); list.add("openid." + axa + ".type.firstname=http://axschema.org/namePerson/first"); list.add("openid." + axa + ".type.lastname=http://axschema.org/namePerson/last"); list.add("openid." + axa + ".type.gender=http://axschema.org/person/gender"); list.add("openid." + axa + ".required=email,fullname,language,firstname,lastname,gender"); String query = Utils.buildQuery(list); authQuery = query; return query; } private String getAssocQuery() { if (assocQuery != null){ return assocQuery; } List<String> list = new ArrayList<String>(); list.add("openid.ns=http://specs.openid.net/auth/2.0"); list.add("openid.mode=associate"); list.add("openid.session_type=" + Association.SESSION_TYPE_NO_ENCRYPTION); list.add("openid.assoc_type=" + Association.ASSOC_TYPE_HMAC_SHA1); String query = Utils.buildQuery(list); assocQuery = query; return query; } private String lookupUrlByName(String name) { checkMap(); if(StringUtils.isNotEmpty(name)){ String provider = name.toLowerCase(); if(provider.contains("-") || provider.contains("_")){ provider = provider.replace("-", "."); provider = provider.replace("_", "."); } if(urlMap.get(provider) != null){ return urlMap.get(provider); } else { provider = provider.replaceAll("[_]", "."); if(urlMap.get(provider) != null){ return urlMap.get(provider); } } } return null; } private String lookupAliasByName(String name) { checkMap(); String alias = aliasMap.get(name); return alias==null ? Endpoint.DEFAULT_ALIAS : alias; } private void checkMap() { if(urlMap.size() == 0){ loadMap(); } if(aliasMap.size() == 0){ loadMap(); } } private void loadMap() { for (Object k : openIdProperties.keySet()) { String key = (String) k; String value = openIdProperties.getProperty(key); if (key.endsWith(OPEN_ID_PROVIDER_ALIAS_SUFIX)) { aliasMap.put(key.substring(0, key.length()-6), value); } else { urlMap.put(key, value); } } } }