Author: lindner
Date: Wed Mar 12 23:02:40 2008
New Revision: 636638
URL: http://svn.apache.org/viewvc?rev=636638&view=rev
Log:
Signed fetch code patch submitted by Brian Eaton for SHINDIG-109
Reviewed and applied. Nice!
Added:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetSigner.java
Wed Mar 12 23:02:40 2008
@@ -18,55 +18,31 @@
*/
package org.apache.shindig.gadgets;
+import org.apache.shindig.util.BlobCrypterException;
+
/**
* A GadgetSigner implementation that just provides dummy data to satisfy
* tests and API calls. Do not use this for any security applications.
*/
public class BasicGadgetSigner implements GadgetSigner {
- private final long timeToLive;
-
- /**
- * [EMAIL PROTECTED]
- */
- public GadgetToken createToken(Gadget gadget) {
- String uri = gadget.getSpec().getUrl().toString();
- long expiry = System.currentTimeMillis() + this.timeToLive;
- return new BasicGadgetToken(uri + '$' + expiry);
- }
/**
* [EMAIL PROTECTED]
- * This implementation only validates non-empty tokens. Empty tokens
- * are considered to always be valid.
+ *
+ * Returns a token with some faked out values.
*/
public GadgetToken createToken(String stringToken) throws GadgetException {
- if (stringToken != null && stringToken.length() != 0) {
- String[] parts = stringToken.split("\\$");
- if (parts.length != 2) {
- throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN,
- "Invalid token format.");
- }
- long expiry = Long.parseLong(parts[1]);
- if (expiry < System.currentTimeMillis()) {
- throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN,
- "Expired token.");
- }
+ try {
+ return new BasicGadgetToken("fakeowner", "fakeviewer", "fakeapp",
+ "fakedomain");
+ } catch (BlobCrypterException e) {
+ throw new GadgetException(GadgetException.Code.INVALID_GADGET_TOKEN, e);
}
- return new BasicGadgetToken(stringToken);
- }
-
- /**
- * Create signer
- * @param timeToLive basic TTL in milliseconds
- */
- public BasicGadgetSigner(long timeToLive) {
- this.timeToLive = timeToLive;
}
/**
* Creates a signer with 24 hour token expiry
*/
public BasicGadgetSigner() {
- this(24L * 60 * 60 * 1000);
}
}
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
Wed Mar 12 23:02:40 2008
@@ -18,16 +18,33 @@
*/
package org.apache.shindig.gadgets;
-import java.net.URL;
-import java.util.Collection;
+import org.apache.shindig.util.BlobCrypter;
+import org.apache.shindig.util.BlobCrypterException;
+
+import java.util.HashMap;
import java.util.Map;
/**
* Primitive token implementation that uses stings as tokens.
*/
class BasicGadgetToken implements GadgetToken {
+ /** serialized form of the token */
private final String token;
-
+
+ /** data from the token */
+ private final Map<String, String> tokenData;
+
+ /** tool to use for signing and encrypting the token */
+ private BlobCrypter crypter = new BlobCrypter(INSECURE_KEY);
+
+ private static final byte[] INSECURE_KEY =
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+ private static final String OWNER_KEY = "o";
+ private static final String APP_KEY = "a";
+ private static final String VIEWER_KEY = "v";
+ private static final String DOMAIN_KEY = "d";
+
/**
* [EMAIL PROTECTED]
*/
@@ -38,17 +55,50 @@
/**
* Generates a token from an input string
* @param token String form of token
+ * @param maxAge max age of the token (in seconds)
+ * @throws BlobCrypterException
*/
- public BasicGadgetToken(String token) {
+ public BasicGadgetToken(String token, int maxAge)
+ throws BlobCrypterException {
this.token = token;
+ this.tokenData = crypter.unwrap(token, maxAge);
+ }
+
+ public BasicGadgetToken(String owner, String viewer, String app,
+ String domain) throws BlobCrypterException {
+ tokenData = new HashMap<String, String>();
+ tokenData.put(OWNER_KEY, owner);
+ tokenData.put(VIEWER_KEY, viewer);
+ tokenData.put(APP_KEY, app);
+ tokenData.put(DOMAIN_KEY, domain);
+ token = crypter.wrap(tokenData);
+ }
+
+ /**
+ * [EMAIL PROTECTED]
+ */
+ public String getAppId() {
+ return tokenData.get(APP_KEY);
+ }
+
+ /**
+ * [EMAIL PROTECTED]
+ */
+ public String getDomain() {
+ return tokenData.get(DOMAIN_KEY);
+ }
+
+ /**
+ * [EMAIL PROTECTED]
+ */
+ public String getOwnerId() {
+ return tokenData.get(OWNER_KEY);
}
/**
* [EMAIL PROTECTED]
- * Signer that does not sign.
*/
- public URL signUrl(URL uri, String httpMethod,
- Map<String, Collection<String>> parameters) {
- return uri;
+ public String getViewerId() {
+ return tokenData.get(VIEWER_KEY);
}
}
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSigner.java
Wed Mar 12 23:02:40 2008
@@ -19,34 +19,13 @@
package org.apache.shindig.gadgets;
/**
- * Handles generation of signing tokens for various request types.
- * Implementations are free to define their own signing parameters in any
- * way that is suitable for their site.
+ * Handles verification of gadget security tokens.
*/
public interface GadgetSigner {
/**
- * Generates a token for the given gadget.
- * Implementations should also add their own user-related context data
- * to the token.
- *
- * @param gadget
- * @return The token representation of the input data.
- */
- public GadgetToken createToken(Gadget gadget);
-
- /**
- * Generates a token from an input string. This call must produce a token
that
- * will validate against a token produced directly from a gadget so that the
- * following function will always returns a valid GadgetToken:
- *
- * <code>
- * GadgetToken testToken(Gadget gadget, GadgetSigner signer) {
- * GadgetToken token = signer.createToken(gadget);
- * return signer.createToken(token.toSerialForm());
- * }
- * </code>
- *
+ * Decrypts and verifies a gadget security token to return a gadget token.
+ *
* @param tokenString String representation of the token to be created.
* @return The token representation of the input data.
* @throws GadgetException If tokenString is not a valid token
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
Wed Mar 12 23:02:40 2008
@@ -18,13 +18,9 @@
*/
package org.apache.shindig.gadgets;
-import java.net.URL;
-import java.util.Collection;
-import java.util.Map;
-
/**
* An abstract representation of a signing token.
- * Use in conjuction with @code GadgetSigner.
+ * Use in conjunction with @code GadgetSigner.
*/
public interface GadgetToken {
@@ -37,15 +33,23 @@
*/
public String toSerialForm();
+ /**
+ * @return the owner from the token, or null if there is none.
+ */
+ public String getOwnerId();
+
+ /**
+ * @return the viewer from the token, or null if there is none.
+ */
+ public String getViewerId();
/**
- * Sign a URL using this token
- * @param uri The URL to sign
- * @param httpMethod The HTTP method used
- * @param parameters associated with the signing request
- * @return The signed URL
- * @throws GadgetException
+ * @return the application id from the token, or null if there is none.
+ */
+ public String getAppId();
+
+ /**
+ * @return the domain from the token, or null if there is none.
*/
- public URL signUrl(URL uri, String httpMethod,
- Map<String, Collection<String>> parameters) throws GadgetException;
+ public String getDomain();
}
Added:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java?rev=636638&view=auto
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
(added)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/NullRequestSigner.java
Wed Mar 12 23:02:40 2008
@@ -0,0 +1,38 @@
+/*
+ * 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.apache.shindig.gadgets;
+
+import java.net.URL;
+
+/**
+ * Request signer interface that does nothing, for convenience of writing
+ * code that may or may not need to sign requests.
+ */
+public class NullRequestSigner implements RequestSigner {
+
+ public String getApprovalUrl() {
+ return null;
+ }
+
+ public Status getSigningStatus() {
+ return Status.OK;
+ }
+
+ @SuppressWarnings("unused")
+ public URL signRequest(String method, URL resource, String postBody) throws
GadgetException {
+ return resource;
+ }
+
+}
Added:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java?rev=636638&view=auto
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
(added)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RequestSigner.java
Wed Mar 12 23:02:40 2008
@@ -0,0 +1,60 @@
+/*
+ * 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.apache.shindig.gadgets;
+
+import java.net.URL;
+
+/**
+ * Interface for generating signed requests and handling responses.
+ *
+ * Most of the functionality here isn't used at the moment, just enough to
+ * implement signed fetch. The status and approval bits will be used for
+ * full OAuth.
+ */
+public interface RequestSigner {
+
+ /**
+ * Whether the signing request succeeded.
+ */
+ public enum Status {
+ OK,
+ APPROVAL_NEEDED,
+ }
+
+ /**
+ * Signs a URL and post body, returning a modified URL including the
+ * signature. The post body is not modified.
+ *
+ * @param method how the request will be sent
+ * @param resource the URL to sign
+ * @param postBody the body of the request (may be null)
+ *
+ * @return a signed URL
+ * @throws GadgetException if an error occurs.
+ */
+ public URL signRequest(String method, URL resource, String postBody)
+ throws GadgetException;
+
+ /**
+ * @return the status associated with the last signing request. May return
+ * APPROVAL_NEEDED if user interaction is required.
+ */
+ public Status getSigningStatus();
+
+ /**
+ * @return the URL the user needs to visit to approve the access request.
+ */
+ public String getApprovalUrl();
+}
Added:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java?rev=636638&view=auto
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
(added)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/SignedFetchRequestSigner.java
Wed Mar 12 23:02:40 2008
@@ -0,0 +1,277 @@
+/*
+ * 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.apache.shindig.gadgets;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuth.Parameter;
+import net.oauth.signature.RSA_SHA1;
+
+import org.apache.shindig.util.Crypto;
+import org.apache.shindig.util.TimeSource;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Implements signed fetch based on the OAuth request signing algorithm.
+ *
+ * Subclasses can override signMessage to use their own crypto if they don't
+ * like the oauth.net code for some reason.
+ *
+ * Instances of this class are only accessed by a single thread at a time,
+ * but instances may be created by multiple threads.
+ */
+public class SignedFetchRequestSigner implements RequestSigner {
+
+ protected static final String OPENSOCIAL_OWNERID = "opensocial_ownerid";
+
+ protected static final String OPENSOCIAL_VIEWERID = "opensocial_viewerid";
+
+ protected static final String OPENSOCIAL_APPID = "opensocial_appid";
+
+ protected static final String XOAUTH_PUBLIC_KEY =
+ "xoauth_signature_publickey";
+
+ protected static final Pattern ALLOWED_PARAM_NAME = Pattern
+ .compile("[\\w_\\-]+");
+
+ protected final TimeSource clock = new TimeSource();
+
+ /**
+ * Authentication token for the user and gadget making the request.
+ */
+ protected GadgetToken authToken;
+
+ /**
+ * Private key we pass to the OAuth RSA_SHA1 algorithm. This can be a
+ * PrivateKey object, or a PEM formatted private key, or a DER encoded byte
+ * array for the private key. (No, really, they accept any of them.)
+ */
+ protected Object privateKeyObject;
+
+ /**
+ * The name of the key, included in the fetch to help with key rotation.
+ */
+ protected String keyName;
+
+ /**
+ * Constructor for subclasses that don't want this code to use their
+ * keys.
+ */
+ protected SignedFetchRequestSigner(GadgetToken authToken) {
+ init(authToken, null, null);
+ }
+
+ /**
+ * Constructor based on signing with the given PrivateKey object.
+ *
+ * @param authToken verified gadget security token
+ * @param keyName name of the key to include in the request
+ * @param privateKey the key to use for the signing
+ */
+ public SignedFetchRequestSigner(GadgetToken authToken, String keyName,
+ PrivateKey privateKey) {
+ init(authToken, keyName, privateKey);
+ }
+
+ /**
+ * Constructor based on signing with the given PrivateKey object.
+ *
+ * @param authToken verified gadget security token
+ * @param keyName name of the key to include in the request
+ * @param privateKey base64 encoded private key
+ */
+ public SignedFetchRequestSigner(GadgetToken authToken, String keyName,
+ String privateKey) {
+ init(authToken, keyName, privateKey);
+ }
+
+ /**
+ * Constructor based on signing with the given PrivateKey object.
+ *
+ * @param authToken verified gadget security token
+ * @param keyName name of the key to include in the request
+ * @param privateKey DER encoded private key
+ */
+ public SignedFetchRequestSigner(GadgetToken authToken, String keyName,
+ byte[] privateKey) {
+ init(authToken, keyName, privateKey);
+ }
+
+ protected void init(GadgetToken authToken, String keyName,
+ Object privateKeyObject) {
+ this.authToken = authToken;
+ this.keyName = keyName;
+ this.privateKeyObject = privateKeyObject;
+ }
+
+ /**
+ * Signed fetch doesn't require approvals, always returns null.
+ */
+ public String getApprovalUrl() {
+ return null;
+ }
+
+ /**
+ * Signed fetch doesn't require approvals, always returns Status.OK
+ */
+ public Status getSigningStatus() {
+ return Status.OK;
+ }
+
+ public URL signRequest(String method, URL resource, String postBody)
+ throws GadgetException {
+ try {
+ // Parse the request into parameters for OAuth signing, stripping out
+ // any OAuth or OpenSocial parameters injected by the client
+ String query = resource.getQuery();
+ resource = removeQuery(resource);
+ List<OAuth.Parameter> queryParams = sanitize(OAuth.decodeForm(query));
+ List<OAuth.Parameter> postParams = sanitize(OAuth.decodeForm(postBody));
+ List<OAuth.Parameter> msgParams = new ArrayList<OAuth.Parameter>();
+ msgParams.addAll(queryParams);
+ msgParams.addAll(postParams);
+
+ // Add the OpenSocial parameters
+ addOpenSocialParams(msgParams);
+
+ // Add the OAuth parameters
+ addOAuthParams(msgParams);
+
+ // Build and sign the OAuthMessage; note that the resource here has
+ // no query string, the parameters are all in msgParams
+ OAuthMessage message = new OAuthMessage(method, resource.toString(),
+ msgParams);
+
+ // Sign the message, this may jump into a subclass
+ signMessage(message);
+
+ // Rebuild the query string, including all of the parameters we added.
+ // We have to be careful not to copy POST parameters into the query.
+ // If post and query parameters share a name, they end up being removed
+ // from the query.
+ HashSet<String> forPost = new HashSet<String>();
+ for (OAuth.Parameter param : postParams) {
+ forPost.add(param.getKey());
+ }
+ List<Map.Entry<String, String>> newQuery =
+ new ArrayList<Map.Entry<String, String>>();
+ for (Map.Entry<String, String> param : message.getParameters()) {
+ if (! forPost.contains(param.getKey())) {
+ newQuery.add(param);
+ }
+ }
+ String finalQuery = OAuth.formEncode(newQuery);
+ return new URL(resource.getProtocol(), resource.getHost(),
+ resource.getPort(), resource.getPath() + "?" + finalQuery);
+ } catch (Exception e) {
+ throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+ e);
+ }
+ }
+
+ private URL removeQuery(URL resource) throws MalformedURLException {
+ return new URL(resource.getProtocol(), resource.getHost(),
+ resource.getPort(), resource.getPath());
+ }
+
+
+ private void addOpenSocialParams(List<Parameter> msgParams) {
+ String owner = authToken.getOwnerId();
+ if (owner != null) {
+ msgParams.add(new OAuth.Parameter(OPENSOCIAL_OWNERID, owner));
+ }
+
+ String viewer = authToken.getViewerId();
+ if (viewer != null) {
+ msgParams.add(new OAuth.Parameter(OPENSOCIAL_VIEWERID, viewer));
+ }
+
+ String app = authToken.getAppId();
+ if (app != null) {
+ msgParams.add(new OAuth.Parameter(OPENSOCIAL_APPID, app));
+ }
+
+ }
+
+ private void addOAuthParams(List<Parameter> msgParams) {
+ msgParams.add(new OAuth.Parameter(OAuth.OAUTH_TOKEN, ""));
+
+ String domain = authToken.getDomain();
+ if (domain != null) {
+ msgParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, domain));
+ }
+
+ if (keyName != null) {
+ msgParams.add(new OAuth.Parameter(XOAUTH_PUBLIC_KEY, keyName));
+ }
+
+ String nonce = Long.toHexString(Crypto.rand.nextLong());
+ msgParams.add(new OAuth.Parameter(OAuth.OAUTH_NONCE, nonce));
+
+ String timestamp = Long.toString(clock.currentTimeMillis()/1000L);
+ msgParams.add(new OAuth.Parameter(OAuth.OAUTH_TIMESTAMP, timestamp));
+
+ msgParams.add(new OAuth.Parameter(OAuth.OAUTH_SIGNATURE_METHOD,
+ OAuth.RSA_SHA1));
+ }
+
+ /**
+ * Sign a message and append the oauth signature parameter to the message
+ * object.
+ *
+ * @param message the message to sign
+ *
+ * @throws Exception because the OAuth libraries require it.
+ */
+ protected void signMessage(OAuthMessage message) throws Exception {
+ OAuthConsumer consumer = new OAuthConsumer(null, null, null, null);
+ consumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKeyObject);
+ OAuthAccessor accessor = new OAuthAccessor(consumer);
+ message.sign(accessor);
+ }
+
+ /**
+ * Strip out any owner or viewer id passed by the client.
+ */
+ private List<Parameter> sanitize(List<Parameter> params) {
+ ArrayList<Parameter> list = new ArrayList<Parameter>();
+ for (Parameter p : params) {
+ if (allowParam(p.getKey())) {
+ list.add(p);
+ }
+ }
+ return list;
+ }
+
+ private boolean allowParam(String paramName) {
+ String canonParamName = paramName.toLowerCase();
+ return (!(canonParamName.startsWith("oauth") ||
+ canonParamName.startsWith("xoauth") ||
+ canonParamName.startsWith("opensocial")) &&
+ ALLOWED_PARAM_NAME.matcher(canonParamName).matches());
+ }
+
+
+}
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CrossServletState.java
Wed Mar 12 23:02:40 2008
@@ -23,6 +23,8 @@
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetServer;
import org.apache.shindig.gadgets.GadgetSigner;
+import org.apache.shindig.gadgets.GadgetToken;
+import org.apache.shindig.gadgets.RequestSigner;
import java.util.Set;
import java.util.logging.Logger;
@@ -101,6 +103,26 @@
* parameters, so caution should be taken when adding your own data.
*/
public abstract String getIframeUrl(Gadget gadget);
+
+ /**
+ * Constructs a RequestSigner object that can be used to sign requests from
+ * the given gadget token to implement signed fetch.
+ *
+ * @param token the decrypted, verified security token
+ * @return a request signer implementing signed fetch.
+ *
+ * @see org.apache.shindig.gadgets.SignedFetchRequestSigner
+ */
+ public abstract RequestSigner makeSignedFetchRequestSigner(GadgetToken
token);
+
+ /**
+ * Constructs a RequestSigner object that can be used to sign requests from
+ * the given gadget token to implement full OAuth.
+ *
+ * @param token the decrypted, verified security token
+ * @return a request signer implementing signed fetch.
+ */
+ public abstract RequestSigner makeOAuthRequestSigner(GadgetToken token);
/**
* Initializes this handler using the provided implementation.
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultCrossServletState.java
Wed Mar 12 23:02:40 2008
@@ -32,9 +32,12 @@
import org.apache.shindig.gadgets.GadgetServerConfig;
import org.apache.shindig.gadgets.GadgetSigner;
import org.apache.shindig.gadgets.GadgetSpecFetcher;
+import org.apache.shindig.gadgets.GadgetToken;
import org.apache.shindig.gadgets.JsLibrary;
import org.apache.shindig.gadgets.MessageBundleFetcher;
import org.apache.shindig.gadgets.RemoteContentFetcher;
+import org.apache.shindig.gadgets.RequestSigner;
+import org.apache.shindig.gadgets.SignedFetchRequestSigner;
import org.apache.shindig.gadgets.SyndicatorConfig;
import org.apache.shindig.gadgets.UserPrefs;
import org.apache.shindig.gadgets.spec.GadgetSpec;
@@ -217,5 +220,34 @@
} catch (GadgetException e) {
throw new ServletException(e);
}
+ }
+
+ @Override
+ public RequestSigner makeOAuthRequestSigner(GadgetToken token) {
+ return null;
+ }
+
+ @Override
+ public RequestSigner makeSignedFetchRequestSigner(GadgetToken token) {
+ // Real implementations should use their own key, probably pulled from
+ // disk rather than hardcoded in the source.
+ final String PRIVATE_KEY_TEXT =
+ "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V" +
+ "A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d" +
+ "7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ" +
+ "hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H" +
+ "X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm" +
+ "uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw" +
+ "rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z" +
+ "zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn" +
+ "qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG" +
+ "WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno" +
+ "cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+" +
+ "3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8" +
+ "AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54" +
+ "Lw03eHTNQghS0A==";
+ final String PRIVATE_KEY_NAME = "shindig-insecure-key";
+ return new SignedFetchRequestSigner(token, PRIVATE_KEY_NAME,
+ PRIVATE_KEY_TEXT);
}
}
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
Wed Mar 12 23:02:40 2008
@@ -24,6 +24,7 @@
import org.apache.shindig.gadgets.RemoteContent;
import org.apache.shindig.gadgets.RemoteContentFetcher;
import org.apache.shindig.gadgets.RemoteContentRequest;
+import org.apache.shindig.gadgets.RequestSigner;
import org.apache.shindig.util.InputStreamConsumer;
import org.json.JSONException;
@@ -150,7 +151,7 @@
return new RemoteContent(HttpServletResponse.SC_UNAUTHORIZED,
"Invalid security token.".getBytes(), null);
}
- signedUrl = signUrl(originalUrl, token, request);
+ signedUrl = signUrl(state, originalUrl, token, request);
} else {
signedUrl = originalUrl;
}
@@ -278,10 +279,12 @@
* @return The signed url.
*/
@SuppressWarnings("unchecked")
- private URL signUrl(URL originalUrl, GadgetToken token,
+ private URL signUrl(CrossServletState state, URL originalUrl, GadgetToken
token,
HttpServletRequest request) throws GadgetException {
String method = getParameter(request, "httpMethod", "GET");
- return token.signUrl(originalUrl, method, request.getParameterMap());
+ String body = getParameter(request, "postBody", null);
+ RequestSigner signer = state.makeSignedFetchRequestSigner(token);
+ return signer.signRequest(method, originalUrl, body);
}
/**
Modified:
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java?rev=636638&r1=636637&r2=636638&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTestFixture.java
Wed Mar 12 23:02:40 2008
@@ -78,6 +78,16 @@
public void init(ServletContext config) {
}
+
+ @Override
+ public RequestSigner makeOAuthRequestSigner(GadgetToken token) {
+ return null;
+ }
+
+ @Override
+ public RequestSigner makeSignedFetchRequestSigner(GadgetToken token) {
+ return null;
+ }
};
public GadgetTestFixture() {
Added:
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java?rev=636638&view=auto
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
(added)
+++
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/SignedFetchRequestSignerTest.java
Wed Mar 12 23:02:40 2008
@@ -0,0 +1,225 @@
+/*
+ * 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.apache.shindig.gadgets;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuth.Parameter;
+import net.oauth.signature.RSA_SHA1;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Tests the signed fetch code.
+ */
+public class SignedFetchRequestSignerTest extends TestCase {
+ private static final String PRIVATE_KEY_TEXT =
+ "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V" +
+ "A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d" +
+ "7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ" +
+ "hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H" +
+ "X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm" +
+ "uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw" +
+ "rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z" +
+ "zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn" +
+ "qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG" +
+ "WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno" +
+ "cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+" +
+ "3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8" +
+ "AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54" +
+ "Lw03eHTNQghS0A==";
+
+ private static final String CERTIFICATE_TEXT =
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0\n" +
+ "IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV\n" +
+ "BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\n" +
+ "gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY\n" +
+ "zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb\n" +
+ "mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3\n" +
+ "DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d\n" +
+ "4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb\n" +
+ "WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J\n" +
+ "-----END CERTIFICATE-----";
+
+ private SignedFetchRequestSigner signer;
+ private BasicGadgetToken authToken;
+ private OAuthAccessor accessor;
+
+ @Override
+ public void setUp() throws Exception {
+ authToken = new BasicGadgetToken("o", "v", "a", "d");
+ signer = new SignedFetchRequestSigner(authToken, "foo", PRIVATE_KEY_TEXT);
+ OAuthConsumer consumer = new OAuthConsumer(null, null, null, null);
+ consumer.setProperty(RSA_SHA1.X509_CERTIFICATE, CERTIFICATE_TEXT);
+ accessor = new OAuthAccessor(consumer);
+ }
+
+ public void testParametersSet() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("GET", unsigned, null);
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertTrue(contains(queryParams, "opensocial_ownerid", "o"));
+ Assert.assertTrue(contains(queryParams, "opensocial_viewerid", "v"));
+ Assert.assertTrue(contains(queryParams, "opensocial_appid", "a"));
+ Assert.assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "d"));
+ Assert.assertTrue(
+ contains(queryParams, "xoauth_signature_publickey", "foo"));
+ }
+
+ public void testTrickyParametersInQuery() throws Exception {
+ String tricky = "%6fpensocial_ownerid=gotcha";
+ URL unsigned = new URL("http://test?" + tricky);
+ URL out = signer.signRequest("GET", unsigned, null);
+ Assert.assertFalse(out.getQuery().contains("gotcha"));
+ assertSignatureOK("GET", out.toString(), null);
+ }
+
+ public void testTrickyParametersInBody() throws Exception {
+ URL unsigned = new URL("http://test");
+ String tricky = "%6fpensocial_ownerid=gotcha";
+ URL out = signer.signRequest("POST", unsigned, tricky);
+ assertSignatureInvalid("POST", out.toString(), tricky);
+ }
+
+ public void testGetNoQuery() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("GET", unsigned, null);
+ assertSignatureOK("GET", out.toString(), null);
+ }
+
+ public void testGetWithQuery() throws Exception {
+ URL unsigned = new URL("http://test?a=b");
+ URL out = signer.signRequest("GET", unsigned, null);
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertTrue(contains(queryParams, "a", "b"));
+ assertSignatureOK("GET", out.toString(), null);
+ }
+
+
+ public void testGetWithQueryMultiParam() throws Exception {
+ URL unsigned = new URL("http://test?a=b&a=c");
+ URL out = signer.signRequest("GET", unsigned, null);
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertTrue(contains(queryParams, "a", "b"));
+ Assert.assertTrue(contains(queryParams, "a", "c"));
+ assertSignatureOK("GET", out.toString(), null);
+ }
+
+ public void testPostNoQueryNoData() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("POST", unsigned, null);
+ assertSignatureOK("POST", out.toString(), null);
+ }
+
+ public void testPostWithQueryNoData() throws Exception {
+ URL unsigned = new URL("http://test?name=value");
+ URL out = signer.signRequest("POST", unsigned, null);
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertTrue(contains(queryParams, "name", "value"));
+ assertSignatureOK("POST", out.toString(), null);
+ }
+
+ public void testPostNoQueryWithData() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("POST", unsigned, "name=value");
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertFalse(contains(queryParams, "name", "value"));
+ assertSignatureOK("POST", out.toString(), "name=value");
+ }
+
+ public void testPostWithQueryWithData() throws Exception {
+ URL unsigned = new URL("http://test?queryName=queryValue");
+ URL out = signer.signRequest("POST", unsigned, "name=value");
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertTrue(contains(queryParams, "queryName", "queryValue"));
+ assertSignatureOK("POST", out.toString(), "name=value");
+ }
+
+ public void testStripOpenSocialParamsFromQuery() throws Exception {
+ URL unsigned = new URL("http://test?opensocial_foo=bar");
+ URL out = signer.signRequest("POST", unsigned, "name=value");
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertFalse(contains(queryParams, "opensocial_foo", "bar"));
+ assertSignatureOK("POST", out.toString(), "name=value");
+ }
+
+ public void testStripOAuthParamsFromQuery() throws Exception {
+ URL unsigned = new URL("http://test?oauth_foo=bar");
+ URL out = signer.signRequest("POST", unsigned, "name=value");
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(out.getQuery());
+ Assert.assertFalse(contains(queryParams, "oauth_foo", "bar"));
+ assertSignatureOK("POST", out.toString(), "name=value");
+ }
+
+ public void testStripOpenSocialParamsFromBody() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("POST", unsigned, "opensocial_foo=bar");
+ assertSignatureInvalid("POST", out.toString(), "opensocial_foo=bar");
+ }
+
+ public void testStripOAuthParamsFromBody() throws Exception {
+ URL unsigned = new URL("http://test");
+ URL out = signer.signRequest("POST", unsigned, "oauth_foo=bar");
+ assertSignatureInvalid("POST", out.toString(), "oauth_foo=bar");
+ }
+
+ private void assertSignatureOK(String method, String urlStr, String body)
+ throws Exception {
+ URL url = new URL(urlStr);
+ URL noQuery = new URL(url.getProtocol(), url.getHost(), url.getPort(),
+ url.getPath());
+ List<OAuth.Parameter> queryParams = OAuth.decodeForm(url.getQuery());
+ List<OAuth.Parameter> postParams = OAuth.decodeForm(body);
+
+ ArrayList<OAuth.Parameter> msgParams = new ArrayList<OAuth.Parameter>();
+ msgParams.addAll(queryParams);
+ msgParams.addAll(postParams);
+
+ OAuthMessage message = new OAuthMessage(method, noQuery.toString(),
+ msgParams);
+
+ // Throws on failure
+ message.validateSignature(accessor);
+ }
+
+ private void assertSignatureInvalid(String method, String urlStr, String
body) {
+ try {
+ assertSignatureOK(method, urlStr, body);
+ fail("Signature verification should have failed");
+ } catch (Exception e) {
+ // good
+ }
+ }
+
+ // Checks whether the given parameter list contains the specified
+ // key/value pair
+ private boolean contains(List<Parameter> params, String key, String value) {
+ for (Parameter p : params) {
+ if (p.getKey().equals(key) && p.getValue().equals(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}