//
// (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.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* Represents an OpenID authentication request.
*/
public class AuthenticationRequest extends Request {
private final static Log log
= LogFactory.getLog(AuthenticationRequest.class);
private Map extendedMap;
private String claimed_id;
private String identity;
private String handle;
private String returnTo;
private String trustRoot;
private SimpleRegistration sreg;
public final static String OPENID_CLAIMED_ID = "openid.claimed_id";
public final static String OPENID_IDENTITY = "openid.identity";
public final static String OPENID_ASSOC_HANDLE = "openid.assoc_handle";
public final static String ID_SELECT = "http://specs.openid.net/auth/2.0/identifier_select";
public final static String CHECKID_IMMEDIATE = "checkid_immediate";
public final static String CHECKID_SETUP = "checkid_setup";
public final static String OPENID_RETURN_TO = "openid.return_to";
/**
* trust_root is the 1.x equivalent to trust_realm in 2.x
*/
public final static String OPENID_TRUST_ROOT = "openid.trust_root";
public final static String OPENID_REALM = "openid.realm";
public static String OPENID_DH_CONSUMER_PUBLIC
= "openid.dh_consumer_public";
public static String OPENID_SESSION_TYPE = "openid.session_type";
public final static String DH_SHA1 = "DH-SHA1";
private static Map statelessMap = new HashMap();
private static AssociationRequest statelessAr;
static {
statelessMap.put(AuthenticationRequest.OPENID_SESSION_TYPE,
AuthenticationRequest.DH_SHA1);
// this value is not used for stateless, but it's not a valid
// association request unless it's there
//
statelessMap.put(AuthenticationRequest.OPENID_DH_CONSUMER_PUBLIC,
Crypto.convertToString(BigInteger.valueOf(1)));
try {
// the request mode is irrelevant
//
statelessAr = new AssociationRequest(statelessMap, "");
} catch (OpenIdException e) {
// should not happen
//
throw new RuntimeException(e);
}
}
/**
* Creates a standard authentication request.
*
* @param identity the openid identity.
* @param returnTo the return_to value.
* @param trustRoot the openid trust_root.
* @param assocHandle the openid association handle.
* @return an AuthenticationRequest.
* @throws OpenIdException if the request cannot be created.
*/
public static AuthenticationRequest create(String identity, String returnTo,
String trustRoot, String assocHandle)
throws OpenIdException {
Map map = new HashMap();
map.put("openid.mode", CHECKID_SETUP);
map.put(OPENID_IDENTITY, identity);
map.put(OPENID_CLAIMED_ID, identity);
map.put(OPENID_RETURN_TO, returnTo);
map.put(OPENID_TRUST_ROOT, trustRoot);
map.put(OPENID_REALM, trustRoot);
map.put(OPENID_NS, OPENID_20_NAMESPACE);
map.put(OPENID_ASSOC_HANDLE, assocHandle);
return new AuthenticationRequest(map, CHECKID_SETUP);
}
AuthenticationRequest(Map map, String mode) throws OpenIdException {
super(map, mode);
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 (OPENID_NS.equals(key)) {
this.ns = value;
} else if (OPENID_IDENTITY.equals(key)) {
this.identity = value;
} else if (OPENID_CLAIMED_ID.equals(key)) {
this.claimed_id = value;
} else if (OPENID_ASSOC_HANDLE.equals(key)) {
this.handle = value;
} else if (OPENID_RETURN_TO.equals(key)) {
this.returnTo = value;
} else if (OPENID_TRUST_ROOT.equals(key)
|| OPENID_REALM.equals(key)) {
this.trustRoot = value;
} 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 = new SimpleRegistration(map);
checkInvariants();
}
Map toMap() {
Map map = super.toMap();
if (claimed_id != null) {
map.put(AuthenticationRequest.OPENID_CLAIMED_ID, claimed_id);
}
map.put(AuthenticationRequest.OPENID_IDENTITY, identity);
map.put(AuthenticationRequest.OPENID_ASSOC_HANDLE, handle);
map.put(AuthenticationRequest.OPENID_RETURN_TO, returnTo);
map.put(AuthenticationRequest.OPENID_TRUST_ROOT, trustRoot);
map.put(AuthenticationRequest.OPENID_REALM, trustRoot);
if (extendedMap != null && !extendedMap.isEmpty()) {
for (Iterator iter = extendedMap.entrySet().iterator();
iter.hasNext();) {
Map.Entry mapEntry = (Map.Entry) iter.next();
String key = (String) mapEntry.getKey();
String value = (String) mapEntry.getValue();
if (value == null) {
continue;
}
// all keys start "openid." in the set
map.put("openid." + key, value);
}
}
return map;
}
/**
* Returns whether this request is immediate, that is, whether the
* authentication mode is "CHECKID_IMMEDIATE".
*
* @return true if this request is immediate; false otherwise.
*/
public boolean isImmediate() {
return AuthenticationRequest.CHECKID_IMMEDIATE
.equals(this.mode);
}
private void checkInvariants() throws OpenIdException {
if (mode == null) {
throw new OpenIdException("Missing mode");
}
if (identity == null) {
throw new OpenIdException("Missing identity");
}
if (claimed_id != null && !this.isVersion2()) {
throw new OpenIdException("claimed_id not valid in version 1.x");
}
if (trustRoot == null) {
if (returnTo != null) {
trustRoot = returnTo;
}
else {
throw new OpenIdException("Missing trust root");
}
}
checkTrustRoot();
Set namespaces = new HashSet();
Set entries = new HashSet();
Set set = extendedMap.entrySet();
for (Iterator iter = set.iterator(); iter.hasNext();) {
Map.Entry mapEntry = (Map.Entry) iter.next();
String key = (String) mapEntry.getKey();
// all keys start "openid." in the set
if (key.startsWith("ns.")) {
key = key.substring(3);
if (OPENID_RESERVED_WORDS.contains(key)) {
throw new OpenIdException("Cannot redefine: " + key);
}
if (namespaces.contains(key)) {
throw new OpenIdException("Multiple definitions: " + key);
}
namespaces.add(key);
} else {
if (entries.contains(key)) {
throw new OpenIdException("Multiple definitions: " + key);
}
entries.add(key);
}
}
// don't check for invalid parameters on 1.x requests; just
// silently ignore them
if (this.isVersion2()) {
for (Iterator iter = entries.iterator(); iter.hasNext();) {
String key = (String) iter.next();
int period = key.indexOf('.');
if (period != -1) {
key = key.substring(0, period);
}
if (!namespaces.contains(key)) {
throw new OpenIdException("No such namespace: " + key);
}
}
}
}
private void checkTrustRoot() throws OpenIdException {
if (trustRoot == null) {
throw new OpenIdException("No " + OPENID_TRUST_ROOT + " given");
}
// URI fragments are not allowed in trustroot
//
if (trustRoot.indexOf('#') > 0) {
throw new OpenIdException("URI fragments are not allowed");
}
// Matched if:
// 1. trustroot and returnto are identical
// 2. trustroot contains wild-card characters "*.", and the
// trailing part of the returnto's domain is identical to the
// part of the trustroot following the "*." wildcard
//
// Trust root Return to
// ---------- ---------
// example.com => example.com ==> ok
// *.example.com => example.com ==> ok
// *.example.com => a.example.com ==> ok
// www.example.com => a.example.com ==> not ok
//
URL r, t;
try {
r = new URL(returnTo);
t = new URL(trustRoot);
} catch (MalformedURLException e) {
throw new OpenIdException("Malformed URL");
}
String tHost = new StringBuffer(t.getHost()).reverse().toString();
String rHost = new StringBuffer(r.getHost()).reverse().toString();
String[] tNames = tHost.split("\\.");
String[] rNames = rHost.split("\\.");
int len = (tNames.length > rNames.length)
? rNames.length : tNames.length;
int i;
for (i = 0; i < len; i += 1) {
if (!(tNames[i].equals(rNames[i]))
&& (!tNames[i].equals("*"))) {
throw new OpenIdException("returnTo not in trustroot set: " +
tNames[i] + ", " + rNames[i]);
}
}
if ((i < tNames.length) && (!tNames[i].equals("*"))) {
throw new OpenIdException("returnTo not in trustroot set: " +
tNames[1]);
}
// The return to path is equal to or a sub-directory of the
// realm's (trustroot's) path.
//
// Trust root Return to
// ---------- ---------
// /a/b/c => /a/b/c/d ==> ok
// /a/b/c => /a/b ==> not ok
// /a/b/c => /a/b/b ==> not ok
//
String tPath = t.getPath();
String rPath = r.getPath();
int n = rPath.indexOf(tPath);
if (n != 0) {
throw new OpenIdException("return to & trust root paths mismatch");
}
// if we're here, we're good to go!
}
public Response processUsing(ServerInfo si) throws OpenIdException {
Store store = si.getStore();
Crypto crypto = si.getCrypto();
Association assoc = null;
String invalidate = null;
if (handle != null) {
assoc = store.findAssociation(handle);
if (assoc != null && assoc.hasExpired()) {
log.info("Association handle has expired: " + handle);
assoc = null;
}
}
if (handle == null || assoc == null) {
log.info("Invalidating association handle: " + handle);
invalidate = handle;
assoc = store.generateAssociation(statelessAr, crypto);
store.saveAssociation(assoc);
}
return new AuthenticationResponse(si, this, assoc, crypto, invalidate);
}
/**
* Returns the identity used in this authentication request.
*
* @return the identity.
*/
public String getIdentity() {
return identity;
}
/**
* 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) {
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 extension object's parameters to the extensions map.
*/
public void addExtension (Extension ext) {
addExtensions(ext.getParamMap());
}
/**
* Returns whether the given identity equals {@link #ID_SELECT}.
*
* @return true if the identity equals {@link #ID_SELECT}.
*/
public boolean isIdentifierSelect() {
return AuthenticationRequest.ID_SELECT.equals(identity);
}
/**
* Returns the claimed identity used in this authentication request.
*
* @return the claimed identity.
*/
public String getClaimedIdentity() {
return claimed_id;
}
/**
* Sets the identity used in this authentication request.
*
* @param identity the identity.
*/
public void setIdentity(String identity) {
this.identity = identity;
}
/**
* Returns the 'return to' address in this authentication request.
*
* @return the address.
*/
public String getReturnTo() {
return returnTo;
}
/**
* Returns the handle used in this authentication request.
*
* @return the handle
*/
public String getHandle() {
return handle;
}
/**
* Returns the trust root address in this authentication request.
*
* @return the address.
*/
public String getTrustRoot() {
return trustRoot;
}
/**
* Returns the simple registration fields in this authentication request.
*
* @return the sreg fields; or null if none present.
*/
public SimpleRegistration getSimpleRegistration() {
return sreg;
}
/**
* Sets the simple registration fields in this authentication request.
*
* @param sreg the registration fields.
*/
public void setSimpleRegistration(SimpleRegistration sreg) {
this.sreg = sreg;
}
public String toString() {
return "[AuthenticationRequest "
+ super.toString()
+ ", sreg=" + sreg
+ ", claimed identity=" + claimed_id
+ ", identity=" + identity
+ ", handle=" + handle + ", return to=" + returnTo
+ ", trust root=" + trustRoot + "]";
}
}