// // (C) Copyright 2007 VeriSign, Inc. All Rights Reserved. // // VeriSign, Inc. shall have no responsibility, financial or // otherwise, for any consequences arising out of the use of // this material. The program material is provided on an "AS IS" // BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. // // Distributed under an Apache License // http://www.apache.org/licenses/LICENSE-2.0 // package org.verisign.joid; import org.verisign.joid.extension.Extension; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; import org.apache.tsik.datatypes.DateTime; /** * Represents an OpenID authentication response. */ public class AuthenticationResponse extends Response { private static Log log = LogFactory.getLog(AuthenticationResponse.class); public static String OPENID_PREFIX = "openid."; public static String OPENID_RETURN_TO = "openid.return_to"; public static String OPENID_OP_ENDPOINT = "openid.op_endpoint"; public static String OPENID_IDENTITY = "openid.identity"; public static String OPENID_ERROR = "openid.error"; public static String OPENID_NONCE = "openid.response_nonce"; public static String OPENID_INVALIDATE_HANDLE = "openid.invalidate_handle"; public static String OPENID_ASSOCIATION_HANDLE = "openid.assoc_handle"; public static String OPENID_SIGNED = "openid.signed"; // package scope so that ResponseFactory can trigger on this key public static String OPENID_SIG = "openid.sig"; private Map extendedMap; private String claimed_id; private String identity; private String returnTo; private String nonce; private String invalidateHandle; private String associationHandle; private String signed; private String algo; private String signature; private SimpleRegistration sreg; private String urlEndPoint; private byte[] key; /** * Returns the signature in this response. * @return the signature in this response. */ public String getSignature(){return signature;} /** * Returns the list of signed elements in this response. * @return the comma-separated list of signed elements in this response. */ public String getSignedList(){return signed;} /** * Returns the association handle in this response. * @return the association handle in this response. */ public String getAssociationHandle(){return associationHandle;} /** * Returns the internal elements mapped to a map. The keys used * are those defined by the specification, for example * <code>openid.mode</code>. * * TODO: Made public only for unit tests. Needs to package-scope * limit this method. * * @return a map with all internal values mapped to their specification * keys. */ public Map toMap() { Map map = super.toMap(); if (isVersion2()) { map.put(AuthenticationResponse.OPENID_OP_ENDPOINT, urlEndPoint); } map.put(AuthenticationResponse.OPENID_MODE, mode); map.put(AuthenticationResponse.OPENID_IDENTITY, identity); map.put(AuthenticationResponse.OPENID_RETURN_TO, returnTo); map.put(AuthenticationResponse.OPENID_NONCE, nonce); if (claimed_id != null) { map.put(AuthenticationRequest.OPENID_CLAIMED_ID, claimed_id); } if (invalidateHandle != null){ map.put(AuthenticationResponse.OPENID_INVALIDATE_HANDLE, invalidateHandle); } map.put(AuthenticationResponse.OPENID_ASSOCIATION_HANDLE, associationHandle); if (signed != null){ map.put(AuthenticationResponse.OPENID_SIGNED, signed); } map.put(AuthenticationResponse.OPENID_SIG, signature); Map sregMap = sreg.getSuppliedValues(); log.debug("sreg in authnresp = "+sreg); Set set = sregMap.entrySet(); for (Iterator iter = set.iterator(); iter.hasNext();){ Map.Entry mapEntry = (Map.Entry) iter.next(); String key = (String) mapEntry.getKey(); String value = (String) mapEntry.getValue(); map.put(SimpleRegistration.OPENID_SREG +"."+ key, value); } if (!set.isEmpty() && isVersion2()) { map.put(Message.OPENID_NS + ".sreg", sreg.getNamespace()); } if (extendedMap != null && !extendedMap.isEmpty()) { set = extendedMap.entrySet(); for (Iterator iter = set.iterator(); iter.hasNext();){ Map.Entry mapEntry = (Map.Entry) iter.next(); String key = (String) mapEntry.getKey(); String value = (String) mapEntry.getValue(); map.put(OPENID_PREFIX + key, value); } } return map; } private String generateNonce() { String crumb = Crypto.generateCrumb(); return DateTime.formatISODateTime(new Date()) + crumb; } /** * Unrolls this response as a string. This string will use encoding * suitable for URLs. The string will use the same namespace as the * incoming request. * * @param req the original request. * @param e any exception that occured while processing <code>req</code>, * may be null. * * @return the response as a string. */ public static String toUrlStringResponse(Request req, OpenIdException e) { Map map = new HashMap(); map.put(AuthenticationResponse.OPENID_MODE, "error"); if (req != null) { if (req.isVersion2()) { map.put(AuthenticationResponse.OPENID_NS, req.getNamespace()); } map.put(AuthenticationResponse.OPENID_ERROR, e.getMessage()); } else { map.put(AuthenticationResponse.OPENID_ERROR, "OpenID request error"); } try { return new AuthenticationResponse(map).toUrlString(); } catch (OpenIdException ex){ // this should never happen log.error(ex); return "internal error"; } } /** * Only public for unit tests. Do not use. */ public String sign(byte[] key, String signed) throws OpenIdException { return sign(this.algo, key, signed); } /** * Signs the elements designated by the signed list with the given key and * returns the result encoded to a string. * * @param algorithm the algorithm to use (HMAC-SHA1, HMAC-SHA256) * @param key the key to sign with (HMAC-SHA1, HMAC-SHA256) * @param signed the comma-separated list of elements to sign. The elements * must be mapped internally. * @return the Base 64 encoded result. * @throws OpenIdException at signature errors, or if the signed list * points to elements that are not mapped. */ public String sign(String algorithm, byte[] key, String signed) throws OpenIdException { Map map = toMap(); log.debug("in sign() map="+map); log.debug("in sign() signed="+signed); StringTokenizer st = new StringTokenizer(signed, ","); StringBuffer sb = new StringBuffer(); while (st.hasMoreTokens()) { String s = st.nextToken(); String name = "openid."+s; String value = (String) map.get(name); if (value == null){ throw new OpenIdException("Cannot sign non-existent mapping: " +s); } sb.append(s); sb.append(':'); sb.append(value); sb.append('\n'); } try { byte[] b; if(algorithm == null){ algorithm = AssociationRequest.HMAC_SHA1; } if (algorithm.equals(AssociationRequest.HMAC_SHA1)){ b = Crypto.hmacSha1(key, sb.toString().getBytes("UTF-8")); } else if (algorithm.equals(AssociationRequest.HMAC_SHA256)){ b = Crypto.hmacSha256(key, sb.toString().getBytes("UTF-8")); } else { throw new OpenIdException("Unknown signature algorithm"); } return Crypto.convertToString(b); } catch (UnsupportedEncodingException e){ throw new OpenIdException(e); } catch (InvalidKeyException e){ throw new OpenIdException(e); } catch (NoSuchAlgorithmException e){ throw new OpenIdException(e); } } /** * throws at errors in signature creation */ AuthenticationResponse(ServerInfo serverInfo, AuthenticationRequest ar, Association a, Crypto crypto, String invalidateHandle) throws OpenIdException { super(null); mode = "id_res"; claimed_id = ar.getClaimedIdentity(); identity = ar.getIdentity(); returnTo = ar.getReturnTo(); ns = ar.getNamespace(); nonce = generateNonce(); this.urlEndPoint = serverInfo.getUrlEndPoint(); this.invalidateHandle = invalidateHandle; //may be null associationHandle = a.getHandle(); signed = "assoc_handle,identity,response_nonce,return_to"; if (claimed_id != null){ signed += ",claimed_id"; } if (isVersion2()) { signed += ",op_endpoint"; } sreg = ar.getSimpleRegistration(); log.debug("sreg="+sreg); if (sreg != null){ Map map = sreg.getSuppliedValues(); log.debug("sreg supplied values="+map); Set set = map.entrySet(); for (Iterator iter = set.iterator(); iter.hasNext();){ Map.Entry mapEntry = (Map.Entry) iter.next(); String key = (String) mapEntry.getKey(); signed += ",sreg." + key; } } key = a.getMacKey(); this.algo = a.getAssociationType(); signature = sign(key, signed); extendedMap = new HashMap(); } public AuthenticationResponse(Map map) throws OpenIdException { super(map); Set set = map.entrySet(); extendedMap = new HashMap(); for (Iterator iter = set.iterator(); iter.hasNext();){ Map.Entry mapEntry = (Map.Entry) iter.next(); String key = (String) mapEntry.getKey(); String value = (String) mapEntry.getValue(); if (AuthenticationResponse.OPENID_MODE.equals(key)) { mode = value; } else if (AuthenticationResponse.OPENID_IDENTITY.equals(key)) { identity = value; } else if (AuthenticationRequest.OPENID_CLAIMED_ID.equals(key)) { claimed_id = value; } else if (AuthenticationResponse.OPENID_RETURN_TO.equals(key)) { returnTo = value; } else if (OPENID_NONCE.equals(key)) { nonce = value; } else if (OPENID_INVALIDATE_HANDLE.equals(key)) { invalidateHandle = value; } else if (OPENID_ASSOCIATION_HANDLE.equals(key)) { associationHandle = value; } else if (OPENID_SIGNED.equals(key)) { signed = value; } else if (OPENID_SIG.equals(key)) { signature = value; } else if (OPENID_OP_ENDPOINT.equals(key)) { urlEndPoint = value; // we get op_endpoint without a 2.0 ns for some // check_auth requests // since we use this class to recalculate the // signature we need to set version 2 explicitly or we // won't include the op_endpoint in the map // (op_endpoint isn't allowed in 1.x responses) if (ns == null) { ns = OPENID_20_NAMESPACE; } } else if (key != null && key.startsWith("openid.")) { String foo = key.substring(7); // remove "openid." if ((!(OPENID_RESERVED_WORDS.contains(foo))) && (!foo.startsWith("sreg."))) { extendedMap.put(foo, value); } } } this.sreg = SimpleRegistration.parseFromResponse(map); log.debug("authn resp constr sreg="+sreg); } /** * Returns the extensions in this authentication request. * * @return the extensions; empty if none. */ public Map getExtensions() { return extendedMap; } /** * Add the extension map to the internal extensions map. * * @param map Map<String, String> of name value pairs */ public void addExtensions (Map map) throws OpenIdException { Iterator it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry mapEntry = (Map.Entry)it.next(); String key = (String)mapEntry.getKey(); String value = (String)mapEntry.getValue(); extendedMap.put(key, value); // add items to signature // signed should already contain a list of base params to sign signed += "," + key; } // recalculate signature signature = sign(key, signed); } /** * Add extension object's parameters to the extensions map. */ public void addExtension (Extension ext) throws OpenIdException { addExtensions(ext.getParamMap()); } public String toString() { String s = "[AuthenticationResponse " + super.toString(); if (sreg != null) { s+=", sreg="+sreg; } s+=", mode="+mode +", algo="+algo +", nonce="+nonce +", association handle="+associationHandle +", invalidation handle="+invalidateHandle +", signed="+signed +", signature="+signature +", identity="+identity +", return to="+returnTo +"]"; return s; } public String getClaimedId() { return claimed_id; } public String getIdentity() { return identity; } public String getReturnTo() { return returnTo; } public String getNonce() { return nonce; } public String getInvalidateHandle() { return invalidateHandle; } public String getSigned() { return signed; } public String getAlgo() { return algo; } public SimpleRegistration getSreg() { return sreg; } public String getUrlEndPoint() { return urlEndPoint; } }