Author: [email protected] Date: Wed Jul 13 12:56:22 2011 New Revision: 1212 Log: [AMDATUAUTH-65] Implemented a custom validator which uses a pluggable nonce storage provider. The oauth project comes with an in-memory storage, which is obviously still not distributed. The new pluggable approach however facilitates implementing a Cassandra storage for nonces, making it distributed.
Added: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/OAuthNonceStorageProvider.java trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/PluggableOAuthValidator.java trunk/amdatu-auth/oauth-stores/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/pom.xml trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/osgi/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/osgi/Activator.java trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/service/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/service/InMemNonceStorageProviderImpl.java trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/test/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/test/unit/ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/test/unit/NonceStoreTest.java trunk/amdatu-auth/oauth-stores/pom.xml Modified: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/osgi/Activator.java trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/OAuthTokenProviderImpl.java trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java trunk/amdatu-auth/test-integration/tests/pom.xml Added: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/OAuthNonceStorageProvider.java ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/OAuthNonceStorageProvider.java Wed Jul 13 12:56:22 2011 @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2010, 2011 The Amdatu Foundation + * + * 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.amdatu.authentication.oauth.server; + +/** + * Pluggable nonce store. + * + * @author ivol + */ +public interface OAuthNonceStorageProvider { + /** + * Adds a nonce to the store and returns true if the store did not yet contain it. If the nonce + * is already contained by the store, the nonce is not added/updated and false is returned. + * Note that two nonces are considered to be the same in case ALL input parameters of this method + * are exactly the same. The sole purpose of the nonce is to prevent replay attacks. If the timestamp + * or any other request parameter in the request is different, it is not a replay attack since it + * is not the same request. + * + * @param timestamp + * The timestamp of the request that contains the nonce + * @param nonce + * The nonce itself + * @param requestParams + * Any additional parameters send along with the request that contained the nonce and should + * be associated with the nonce. Note that two nonces with different request parameters are + * not considered to be the same. For example, adding the consumer key as request parameter + * and associating it with a nonce prevents that if two service consumers both use the + * auto-increment index (1,2,3,...) approach to generate the nonce, they would run into + * conflicts as they are trying to use the same nonces. Note that such approach would be a + * bad design decision, a nonce is supposed to be a long and random number or string. + + * @return true if the nonce did not yet exist and was added to the store, false otherwise + */ + boolean addNonce(long timestamp, String nonce, String[] requestParams); + + /** + * Removes expired nonces. All nonces which have a timestamp that is older (smaller: <) then the provided timestamp + * are removed. + * + * @param timestamp + */ + void removeExpiredNonces(long timestamp); +} Modified: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/osgi/Activator.java ============================================================================== --- trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/osgi/Activator.java (original) +++ trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/osgi/Activator.java Wed Jul 13 12:56:22 2011 @@ -24,6 +24,7 @@ import org.amdatu.authentication.oauth.api.OAuthServiceProvider; import org.amdatu.authentication.oauth.server.OAuthAccessTokenServlet; import org.amdatu.authentication.oauth.server.OAuthAuthorizeTokenServlet; +import org.amdatu.authentication.oauth.server.OAuthNonceStorageProvider; import org.amdatu.authentication.oauth.server.OAuthRequestTokenServlet; import org.amdatu.authentication.oauth.server.OAuthServerConfig; import org.amdatu.authentication.oauth.server.OAuthServiceConsumerRegistryREST; @@ -87,6 +88,7 @@ .add(createServiceDependency().setService(TokenStorageProvider.class).setRequired(true)) .add(createServiceDependency().setService(OAuthServiceProvider.class).setRequired(true)) .add(createServiceDependency().setService(OAuthServerConfiguration.class).setRequired(true)) + .add(createServiceDependency().setService(OAuthNonceStorageProvider.class).setRequired(true)) .add(createServiceDependency().setService(OAuthServiceConsumerRegistry.class).setRequired(true))); // Create and register the resource provider Modified: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/OAuthTokenProviderImpl.java ============================================================================== --- trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/OAuthTokenProviderImpl.java (original) +++ trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/OAuthTokenProviderImpl.java Wed Jul 13 12:56:22 2011 @@ -26,13 +26,12 @@ import net.oauth.OAuthException; import net.oauth.OAuthMessage; import net.oauth.OAuthProblemException; -import net.oauth.OAuthValidator; -import net.oauth.SimpleOAuthValidator; import org.amdatu.authentication.oauth.api.ConsumerRegistryStorageException; import org.amdatu.authentication.oauth.api.OAuthServiceConsumer; import org.amdatu.authentication.oauth.api.OAuthServiceConsumerRegistry; import org.amdatu.authentication.oauth.api.OAuthServiceProvider; +import org.amdatu.authentication.oauth.server.OAuthNonceStorageProvider; import org.amdatu.authentication.oauth.server.OAuthTokenProvider; import org.amdatu.authentication.tokenprovider.Token; import org.amdatu.authentication.tokenprovider.TokenStorageProvider; @@ -50,9 +49,10 @@ private volatile OAuthServiceConsumerRegistry m_consumerRegistry; private volatile TokenStorageProvider m_tokenStore; private volatile OAuthServerConfiguration m_config; + private volatile OAuthNonceStorageProvider m_nonceStore; // The simple oAuth validator. - private OAuthValidator m_oAuthValidator; + private PluggableOAuthValidator m_oAuthValidator; // Other private members private String m_tenantId; @@ -69,7 +69,7 @@ // we replace it with the maximum long value (which equals 292 million years, quite close to infinite) requestMaxAge = Long.MAX_VALUE; } - m_oAuthValidator = new SimpleOAuthValidator(requestMaxAge, Double.parseDouble(OAuth.VERSION_1_0)); + m_oAuthValidator = new PluggableOAuthValidator(requestMaxAge, Double.parseDouble(OAuth.VERSION_1_0), m_nonceStore); m_logService.log(LogService.LOG_DEBUG, "OAuthTokenProvider service started for tenant '" + m_tenantId + "'"); } Added: trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/PluggableOAuthValidator.java ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-server/src/main/java/org/amdatu/authentication/oauth/server/service/PluggableOAuthValidator.java Wed Jul 13 12:56:22 2011 @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2010, 2011 The Amdatu Foundation + * + * 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.amdatu.authentication.oauth.server.service; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.oauth.OAuth; +import net.oauth.OAuthAccessor; +import net.oauth.OAuthException; +import net.oauth.OAuthMessage; +import net.oauth.OAuthProblemException; +import net.oauth.OAuthValidator; +import net.oauth.signature.OAuthSignatureMethod; + +import org.amdatu.authentication.oauth.server.OAuthNonceStorageProvider; + +public class PluggableOAuthValidator implements OAuthValidator { + + /** The default maximum age of timestamps is 5 minutes. */ + public static final long DEFAULT_MAX_TIMESTAMP_AGE = 5 * 60 * 1000L; + public static final long DEFAULT_TIMESTAMP_WINDOW = DEFAULT_MAX_TIMESTAMP_AGE; + + /** + * Names of parameters that may not appear twice in a valid message. + * This limitation is specified by OAuth Core <a + * href="http://oauth.net/core/1.0#anchor7">section 5</a>. + */ + public static final Set<String> SINGLE_PARAMETERS = constructSingleParameters(); + + private static Set<String> constructSingleParameters() { + Set<String> s = new HashSet<String>(); + for (String p : new String[] {OAuth.OAUTH_CONSUMER_KEY, OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET, + OAuth.OAUTH_CALLBACK, OAuth.OAUTH_SIGNATURE_METHOD, OAuth.OAUTH_SIGNATURE, OAuth.OAUTH_TIMESTAMP, + OAuth.OAUTH_NONCE, OAuth.OAUTH_VERSION}) { + s.add(p); + } + return Collections.unmodifiableSet(s); + } + + protected final double m_minVersion = 1.0; + protected final double m_maxVersion; + protected final long m_maxTimestampAgeMsec; + private OAuthNonceStorageProvider m_nonceStore; + + /** + * Construct a validator that rejects messages more than five minutes old or + * with a OAuth version other than 1.0. + */ + public PluggableOAuthValidator(OAuthNonceStorageProvider nonceStore) { + this(DEFAULT_TIMESTAMP_WINDOW, Double.parseDouble(OAuth.VERSION_1_0), nonceStore); + } + + /** + * Public constructor. + * + * @param maxTimestampAgeMsec + * the range of valid timestamps, in milliseconds into the past + * or future. So the total range of valid timestamps is twice + * this value, rounded to the nearest second. + * @param maxVersion + * the maximum valid oauth_version + */ + public PluggableOAuthValidator(long maxTimestampAgeMsec, double maxVersion, OAuthNonceStorageProvider nonceStore) { + m_maxTimestampAgeMsec = maxTimestampAgeMsec; + m_maxVersion = maxVersion; + m_nonceStore = nonceStore; + } + + /** + * Remove usedNonces with timestamps that are too old to be valid. + */ + private void removeOldNonces(long currentTimeMsec) { + long minTimestamp = currentTimeMsec - m_maxTimestampAgeMsec; + m_nonceStore.removeExpiredNonces(minTimestamp); + } + + /** + * {@inherit} + * + * @throws URISyntaxException + */ + public void validateMessage(OAuthMessage message, OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException { + checkSingleParameters(message); + validateVersion(message); + validateTimestampAndNonce(message); + validateSignature(message, accessor); + } + + /** Throw an exception if any SINGLE_PARAMETERS occur repeatedly. */ + protected void checkSingleParameters(OAuthMessage message) throws IOException, OAuthException { + // Check for repeated oauth_ parameters: + boolean repeated = false; + Map<String, Collection<String>> nameToValues = new HashMap<String, Collection<String>>(); + for (Map.Entry<String, String> parameter : message.getParameters()) { + String name = parameter.getKey(); + if (SINGLE_PARAMETERS.contains(name)) { + Collection<String> values = nameToValues.get(name); + if (values == null) { + values = new ArrayList<String>(); + nameToValues.put(name, values); + } + else { + repeated = true; + } + values.add(parameter.getValue()); + } + } + if (repeated) { + Collection<OAuth.Parameter> rejected = new ArrayList<OAuth.Parameter>(); + for (Map.Entry<String, Collection<String>> p : nameToValues.entrySet()) { + String name = p.getKey(); + Collection<String> values = p.getValue(); + if (values.size() > 1) { + for (String value : values) { + rejected.add(new OAuth.Parameter(name, value)); + } + } + } + OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.PARAMETER_REJECTED); + problem.setParameter(OAuth.Problems.OAUTH_PARAMETERS_REJECTED, OAuth.formEncode(rejected)); + throw problem; + } + } + + protected void validateVersion(OAuthMessage message) + throws OAuthException, IOException { + String versionString = message.getParameter(OAuth.OAUTH_VERSION); + if (versionString != null) { + double version = Double.parseDouble(versionString); + if (version < m_minVersion || m_maxVersion < version) { + OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.VERSION_REJECTED); + problem.setParameter(OAuth.Problems.OAUTH_ACCEPTABLE_VERSIONS, m_minVersion + "-" + m_maxVersion); + throw problem; + } + } + } + + /** + * Throw an exception if the timestamp is out of range or the nonce has been + * validated previously. + */ + protected void validateTimestampAndNonce(OAuthMessage message) + throws IOException, OAuthProblemException { + message.requireParameters(OAuth.OAUTH_TIMESTAMP, OAuth.OAUTH_NONCE); + long timestamp = Long.parseLong(message.getParameter(OAuth.OAUTH_TIMESTAMP)); + long now = currentTimeMsec(); + validateTimestamp(message, timestamp, now); + validateNonce(message, timestamp, now); + } + + /** Throw an exception if the timestamp [sec] is out of range. */ + protected void validateTimestamp(OAuthMessage message, long timestamp, long currentTimeMsec) throws IOException, + OAuthProblemException { + long min = (currentTimeMsec - m_maxTimestampAgeMsec + 500) / 1000L; + long max = (currentTimeMsec + m_maxTimestampAgeMsec + 500) / 1000L; + if (timestamp < min || max < timestamp) { + OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.TIMESTAMP_REFUSED); + problem.setParameter(OAuth.Problems.OAUTH_ACCEPTABLE_TIMESTAMPS, min + "-" + max); + throw problem; + } + } + + /** + * Throw an exception if the nonce has been validated previously. + * + * @return the earliest point in time at which a call to releaseGarbage + * will actually release some garbage, or null to indicate there's + * nothing currently stored that will become garbage in future. + */ + protected void validateNonce(OAuthMessage message, long timestamp, long currentTimeMsec) throws IOException, + OAuthProblemException { + String nonce = message.getParameter(OAuth.OAUTH_NONCE); + // Associate the consumer key and token with the nonce, such that requests from a different + // consumer or using a different token will never result in a nonce conflict (which could happen + // if they use the same nonce by coincidence) + String[] requestParams = new String[]{message.getConsumerKey(), message.getToken()}; + boolean valid = m_nonceStore.addNonce(currentTimeMsec, nonce, requestParams); + if (!valid) { + throw new OAuthProblemException(OAuth.Problems.NONCE_USED); + } + removeOldNonces(currentTimeMsec); + } + + protected void validateSignature(OAuthMessage message, OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException { + message.requireParameters(OAuth.OAUTH_CONSUMER_KEY, + OAuth.OAUTH_SIGNATURE_METHOD, OAuth.OAUTH_SIGNATURE); + OAuthSignatureMethod.newSigner(message, accessor).validate(message); + } + + /** Get the number of milliseconds since midnight, January 1, 1970 UTC. */ + protected long currentTimeMsec() { + return System.currentTimeMillis(); + } +} Added: trunk/amdatu-auth/oauth-stores/nonce-store-mem/pom.xml ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-stores/nonce-store-mem/pom.xml Wed Jul 13 12:56:22 2011 @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (c) 2010, 2011 The Amdatu Foundation + + 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.verning permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.amdatu.auth</groupId> + <artifactId>org.amdatu.auth.oauth.stores</artifactId> + <version>0.2.0-SNAPSHOT</version> + </parent> + <artifactId>org.amdatu.auth.oauth.store.nonce.mem</artifactId> + <packaging>bundle</packaging> + <name>Amdatu Auth - OAuth filebased nonce store</name> + <description>This bundle implements a persistent storage for oAuth nonces</description> + + <dependencies> + <dependency> + <groupId>org.amdatu.auth</groupId> + <artifactId>org.amdatu.auth.oauth.server</artifactId> + <type>bundle</type> + </dependency> + <dependency> + <groupId>org.amdatu.libraries</groupId> + <artifactId>org.amdatu.libraries.fsstorage</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <configuration> + <instructions> + <Bundle-Activator>org.amdatu.oauth.store.nonce.mem.osgi.Activator</Bundle-Activator> + <Bundle-SymbolicName>org.amdatu.oauth.store.nonce.mem</Bundle-SymbolicName> + <Embed-Dependency>*;scope=compile</Embed-Dependency> + <Import-Package> + org.amdatu.authentication.oauth.server, + * + </Import-Package> + <Export-Package></Export-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> +</project> Added: trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/osgi/Activator.java ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/osgi/Activator.java Wed Jul 13 12:56:22 2011 @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010, 2011 The Amdatu Foundation + * + * 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.amdatu.oauth.store.nonce.mem.osgi; + +import org.amdatu.authentication.oauth.server.OAuthNonceStorageProvider; +import org.amdatu.oauth.store.nonce.mem.service.InMemNonceStorageProviderImpl; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * This class represents the OSGi activator for the tenant service fs storage provider. + */ +public final class Activator extends DependencyActivatorBase { + + @Override + public void init(final BundleContext context, final DependencyManager manager) throws Exception { + + manager.add( + createComponent() + .setImplementation(InMemNonceStorageProviderImpl.class) + .setInterface(OAuthNonceStorageProvider.class.getName(), null) + .add(createServiceDependency().setService(LogService.class).setRequired(true))); + } + + @Override + public void destroy(final BundleContext context, final DependencyManager manager) throws Exception { + } +} Added: trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/service/InMemNonceStorageProviderImpl.java ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/main/java/org/amdatu/oauth/store/nonce/mem/service/InMemNonceStorageProviderImpl.java Wed Jul 13 12:56:22 2011 @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2010, 2011 The Amdatu Foundation + * + * 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.amdatu.oauth.store.nonce.mem.service; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +import org.amdatu.authentication.oauth.server.OAuthNonceStorageProvider; + +public class InMemNonceStorageProviderImpl implements OAuthNonceStorageProvider { + private final Set<UsedNonce> usedNonces = new TreeSet<UsedNonce>(); + + public boolean addNonce(long timestamp, String nonce, String[] requestParams) { + String[] nonceEtc = requestParams != null ? new String[requestParams.length + 1] : new String[1]; + nonceEtc[0] = nonce; + if (requestParams != null) { + for (int i = 0; i < requestParams.length; i++) { + nonceEtc[i + 1] = requestParams[i]; + } + } + UsedNonce usedNonce = new UsedNonce(timestamp, nonceEtc); + return usedNonces.add(usedNonce); + } + + public void removeExpiredNonces(long timestamp) { + UsedNonce min = new UsedNonce(timestamp, new String[0]); + synchronized (usedNonces) { + // Because usedNonces is a TreeSet, its iterator produces + // elements from oldest to newest (their natural order). + for (Iterator<UsedNonce> iter = usedNonces.iterator(); iter.hasNext();) { + UsedNonce used = iter.next(); + if (min.compareTo(used) <= 0) { + break; // all the rest are also new enough + } + iter.remove(); // too old + } + } + } + + /** + * Selected parameters from an OAuth request, in a form suitable for + * detecting duplicate requests. The implementation is optimized for the + * comparison operations (compareTo, equals and hashCode). + * + * @author John Kristian + */ + private static class UsedNonce implements Comparable<UsedNonce> { + private static final String ENCODING = "UTF-8"; + + private final String sortKey; + + /** + * Construct an object containing the given timestamp, nonce and other + * parameters. The order of parameters is significant. + */ + UsedNonce(long timestamp, String[] nonceEtc) { + StringBuilder key = new StringBuilder(String.format("%20d", Long.valueOf(timestamp))); + // The blank padding ensures that timestamps are compared as numbers. + for (String etc : nonceEtc) { + key.append("&").append(etc == null ? " " : percentEncode(etc)); + // A null value is different from "" or any other String. + } + sortKey = key.toString(); + } + + private static String percentEncode(String s) { + if (s == null) { + return ""; + } + try { + return URLEncoder.encode(s, ENCODING) + // OAuth encodes some characters differently: + .replace("+", "%20").replace("*", "%2A") + .replace("%7E", "~"); + // This could be done faster with more hand-crafted code. + } + catch (UnsupportedEncodingException wow) { + throw new RuntimeException(wow.getMessage(), wow); + } + } + + /** + * Determine the relative order of <code>this</code> and + * <code>that</code>, as specified by Comparable. The timestamp is most + * significant; that is, if the timestamps are different, return 1 or + * -1. If <code>this</code> contains only a timestamp (with no nonce + * etc.), return -1 or 0. The treatment of the nonce etc. is murky, + * although 0 is returned only if they're all equal. + */ + public int compareTo(UsedNonce that) { + return (that == null) ? 1 : sortKey.compareTo(that.sortKey); + } + + @Override + public int hashCode() { + return sortKey.hashCode(); + } + + /** + * Return true iff <code>this</code> and <code>that</code> contain equal + * timestamps, nonce etc., in the same order. + */ + @Override + public boolean equals(Object that) { + if (that == null) + return false; + if (that == this) + return true; + if (that.getClass() != getClass()) + return false; + return sortKey.equals(((UsedNonce) that).sortKey); + } + + @Override + public String toString() { + return sortKey; + } + } +} Added: trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/test/unit/NonceStoreTest.java ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-stores/nonce-store-mem/src/test/java/org/amdatu/auth/oauth/test/unit/NonceStoreTest.java Wed Jul 13 12:56:22 2011 @@ -0,0 +1,96 @@ +/* + Copyright (C) 2010 Amdatu.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package org.amdatu.auth.oauth.test.unit; + +import org.amdatu.oauth.store.nonce.mem.service.InMemNonceStorageProviderImpl; +import org.junit.Assert; +import org.junit.Test; + +public class NonceStoreTest { + @Test + public void test() throws Exception { + InMemNonceStorageProviderImpl store = new InMemNonceStorageProviderImpl(); + + String nonce = "A"; + long timestamp1 = 10000; // second 10 + long timestamp2 = 10001; + long timestamp3 = 10002; + long timestamp4 = 20000; + long timestamp5 = 2435436; + + // Validate that the same nonce can be added only once + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp1, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp2, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp2, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertFalse(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertTrue(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertTrue(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp1, no nonces should have been removed + store.removeExpiredNonces(timestamp1); + Assert.assertFalse(store.addNonce(timestamp1, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp2, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertFalse(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp2, timestamp1 nonces should have been removed + store.removeExpiredNonces(timestamp2); + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp2, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertFalse(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp3, timestamp1 and timestamp2 nonces should have been removed + store.removeExpiredNonces(timestamp3); + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp2, nonce, null)); + Assert.assertFalse(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertFalse(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp4, timestamp1, timestamp2 and timestamp3 nonces should have been removed + store.removeExpiredNonces(timestamp4); + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp2, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertFalse(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp5, timestamp1, timestamp2, timestamp3 and timestamp4 + // nonces should have been removed + store.removeExpiredNonces(timestamp5); + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp2, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertTrue(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertFalse(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + + // Validate nonce removal of timestamp5+1, all nonces should have been removed + store.removeExpiredNonces(timestamp5+1); + Assert.assertTrue(store.addNonce(timestamp1, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp2, nonce, null)); + Assert.assertTrue(store.addNonce(timestamp3, nonce, new String[]{"arg1"})); + Assert.assertTrue(store.addNonce(timestamp4, nonce, new String[]{"arg2"})); + Assert.assertTrue(store.addNonce(timestamp5, nonce, new String[]{"arg1", "arg2"})); + } +} Added: trunk/amdatu-auth/oauth-stores/pom.xml ============================================================================== --- (empty file) +++ trunk/amdatu-auth/oauth-stores/pom.xml Wed Jul 13 12:56:22 2011 @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (c) 2010, 2011 The Amdatu Foundation + + 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.verning permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.amdatu.auth</groupId> + <artifactId>org.amdatu.auth</artifactId> + <version>0.2.0-SNAPSHOT</version> + </parent> + <artifactId>org.amdatu.auth.oauth.stores</artifactId> + <packaging>pom</packaging> + <name>Amdatu Auth - OAuth stores</name> + <description>This module holds OAuth storage implementations</description> + + <modules> + <module>nonce-store-mem</module> + </modules> +</project> Modified: trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java ============================================================================== --- trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java (original) +++ trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java Wed Jul 13 12:56:22 2011 @@ -48,6 +48,7 @@ mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.oauth.api").versionAsInProject(), mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.oauth.client").versionAsInProject(), mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.oauth.server").versionAsInProject(), + mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.oauth.store.nonce.mem").versionAsInProject(), mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.oauth.consumerregistry-fs").versionAsInProject(), mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.login.service").versionAsInProject(), mavenBundle().groupId("org.amdatu.auth").artifactId("org.amdatu.auth.useradmin.rest").versionAsInProject() Modified: trunk/amdatu-auth/test-integration/tests/pom.xml ============================================================================== --- trunk/amdatu-auth/test-integration/tests/pom.xml (original) +++ trunk/amdatu-auth/test-integration/tests/pom.xml Wed Jul 13 12:56:22 2011 @@ -147,6 +147,13 @@ <scope>compile</scope> <type>bundle</type> </dependency> + <dependency> + <groupId>org.amdatu.auth</groupId> + <artifactId>org.amdatu.auth.oauth.store.nonce.mem</artifactId> + <version>${project.version}</version> + <scope>test</scope> + <type>bundle</type> + </dependency> </dependencies> </dependencyManagement> @@ -207,6 +214,11 @@ </dependency> <dependency> <groupId>org.amdatu.auth</groupId> + <artifactId>org.amdatu.auth.oauth.store.nonce.mem</artifactId> + <type>bundle</type> + </dependency> + <dependency> + <groupId>org.amdatu.auth</groupId> <artifactId>org.amdatu.auth.login.service</artifactId> <type>bundle</type> </dependency> _______________________________________________ Amdatu-commits mailing list [email protected] http://lists.amdatu.org/mailman/listinfo/amdatu-commits
