Author: scottbw
Date: Tue Feb  4 19:12:48 2014
New Revision: 1564424

URL: http://svn.apache.org/r1564424
Log:
added first effort at creating an encrypted token to replace WidgetInstance as 
a means of storing and communicating information about a widget, in a similar 
wy to Apache Shindig. This code isn't integrated into the rest of the server, 
but serves as a first step for checking the model and testing encryption and 
decryption

Added:
    wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/
    
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthToken.java
    
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenCrypter.java
    
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenUtils.java
    
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/InvalidAuthTokenException.java
    wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/
    
wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/AuthTokenUtilsTest.java

Added: 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthToken.java
URL: 
http://svn.apache.org/viewvc/wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthToken.java?rev=1564424&view=auto
==============================================================================
--- 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthToken.java 
(added)
+++ 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthToken.java 
Tue Feb  4 19:12:48 2014
@@ -0,0 +1,189 @@
+/*
+ * 
+ * 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.wookie.auth;
+
+import org.apache.wookie.server.security.ApiKey;
+
+/**
+ * An AuthToken used to pass contextual information about an instance of a
+ * widget, including the viewer, widget identifier, shared data context, and 
API
+ * key
+ * 
+ * This implementation is based on the Apache Shindig basic token model
+ */
+public class AuthToken {
+
+       private ApiKey apiKey;
+       private String widgetId;
+       private String viewerId;
+       private String contextId;
+       private String lang;
+
+       public static final int DEFAULT_MAX_TOKEN_TTL = 3600; // 1 hour
+       private static final long CLOCK_SKEW_ALLOWANCE = 180; // allow three 
minutes for clock skew
+       private Long expiresAt;
+       private int tokenTTL;
+
+       /**
+        * @return The time in seconds since epoc that this token expires or
+        *         <code>null</code> if unknown or indeterminate.
+        */
+       Long getExpiresAt(){
+               return expiresAt;
+       }
+
+       /**
+        * @return true if the token is no longer valid.
+        */
+       boolean isExpired(){
+               Long expiresAt = getExpiresAt();
+               if (expiresAt != null) {
+                       long maxTime = expiresAt + CLOCK_SKEW_ALLOWANCE;
+                       long now = System.currentTimeMillis() / 1000;
+
+                       if (!(now < maxTime)) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * @return the apiKey value
+        */
+       public String getApiKey() {
+               return apiKey.getValue();
+       }
+       
+       /**
+        * @return the apiKey object
+        */
+       public ApiKey getApiKeyInstance() {
+               return apiKey;
+       }
+
+       /**
+        * @param apiKey
+        *            the apiKey to set
+        */
+       public void setApiKey(ApiKey apiKey) {
+               this.apiKey = apiKey;
+       }
+
+       /**
+        * @return the widgetId
+        */
+       public String getWidgetId() {
+               return widgetId;
+       }
+
+       /**
+        * @param widgetId
+        *            the widgetId to set
+        */
+       public void setWidgetId(String widgetId) {
+               this.widgetId = widgetId;
+       }
+
+       /**
+        * @return the viewerId
+        */
+       public String getViewerId() {
+               return viewerId;
+       }
+
+       /**
+        * @param viewerId
+        *            the viewerId to set
+        */
+       public void setViewerId(String viewerId) {
+               this.viewerId = viewerId;
+       }
+
+       /**
+        * @return the contextId
+        */
+       public String getContextId() {
+               return contextId;
+       }
+
+       /**
+        * @param contextId
+        *            the contextId to set
+        */
+       public void setContextId(String contextId) {
+               this.contextId = contextId;
+       }
+
+       /**
+        * @return the lang
+        */
+       public String getLang() {
+               return lang;
+       }
+
+       /**
+        * @param lang
+        *            the lang to set
+        */
+       public void setLang(String lang) {
+               this.lang = lang;
+       }
+
+
+       /**
+        * Compute and set the expiration time for this token using the default 
TTL.
+        *
+        * @return This security token.
+        * @see #setExpires(int)
+        */
+       protected void setExpires() {
+               setExpires(DEFAULT_MAX_TOKEN_TTL);
+       }
+
+       /**
+        * Compute and set the expiration time for this token using the 
provided TTL.
+        *
+        * @param tokenTTL the time to live (in seconds) of the token
+        * @return This security token.
+        */
+       protected void setExpires(int tokenTTL) {
+               this.tokenTTL = tokenTTL;
+               setExpiresAt( (System.currentTimeMillis() / 1000) + 
getMaxTokenTTL());
+       }
+
+       /**
+        * Set the expiration time for this token.
+        *
+        * @param expiresAt When this token expires, in seconds since epoch.
+        * @return This security token.
+        */
+       protected void setExpiresAt(Long expiresAt) {
+               this.expiresAt = expiresAt;
+       }
+
+       /**
+        * Returns the maximum allowable time (in seconds) for this token to 
live. 
+        *
+        * @return Maximum allowable time in seconds for a token to live.
+        */
+       protected int getMaxTokenTTL() {
+               return this.tokenTTL;
+       }
+
+}

Added: 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenCrypter.java
URL: 
http://svn.apache.org/viewvc/wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenCrypter.java?rev=1564424&view=auto
==============================================================================
--- 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenCrypter.java
 (added)
+++ 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenCrypter.java
 Tue Feb  4 19:12:48 2014
@@ -0,0 +1,429 @@
+/*
+ * 
+ * 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.wookie.auth;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Basic encryption utility class
+ * 
+ * Using the default constructor, a new token_key file is created with a secure
+ * random key if one does not already exist.
+ */
+public class AuthTokenCrypter {
+       // Labels for key derivation
+       private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
+       private static final int HMAC_SHA1_LEN = 20;
+       private final static String CIPHER_TYPE = "AES/CBC/PKCS5Padding";
+       private final static String CIPHER_KEY_TYPE = "AES";
+       private static final byte CIPHER_KEY_LABEL = 0;
+       private static final int CIPHER_BLOCK_SIZE = 16;
+       private static final byte HMAC_KEY_LABEL = 1;
+       private final static String HMAC_TYPE = "HMACSHA1";
+       private final static int MIN_HMAC_KEY_LEN = 8;
+
+       /** minimum length of master key */
+       public static final int MASTER_KEY_MIN_LEN = 16;
+
+       private byte[] cipherKey;
+       private byte[] hmacKey;
+
+       /**
+        * Creates a crypter based on a key in a file. The key is the first 
line in
+        * the file, whitespace trimmed from either end, as UTF-8 bytes.
+        * 
+        * The following *nix command line will create an excellent key:
+        * 
+        * <pre>
+        * dd if=/dev/random bs=32 count=1  | openssl base64 > /tmp/key.txt
+        * </pre>
+        * 
+        * @throws IOException
+        *             if the file can't be read.
+        */
+       private void init(File keyfile) throws IOException {
+               BufferedReader reader = null;
+               try {
+                       FileInputStream openFile = new FileInputStream(keyfile);
+                       reader = new BufferedReader(
+                                       new InputStreamReader(openFile, 
"UTF-8"));
+                       init(reader.readLine());
+               } finally {
+                       try {
+                               if (reader != null) {
+                                       reader.close();
+                               }
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               }
+       }
+
+       /**
+        * Get a default crypter from the "token_key" file. If no file
+        * exists, generates a new random key and saves it
+        * @throws IOException
+        *             if there is a problem getting or creating a token_key 
file
+        */
+       public AuthTokenCrypter() throws IOException {
+               File keyFile = new File("token_key");
+               if (!keyFile.exists()) {
+                       keyFile.createNewFile();
+                       final SecureRandom random = new SecureRandom();
+                       String key = new BigInteger(130, random).toString(32);
+                       FileWriter writer = new FileWriter(keyFile);
+                       writer.write(key);
+                       writer.append("\n#\n# This is the key used to encrypt 
tokens\n#");
+                       writer.flush();
+                       writer.close();
+               }
+               init(keyFile);
+       }
+
+       private void init(String masterKey) {
+               if (masterKey == null) {
+                       throw new IllegalArgumentException("Unexpectedly empty 
masterKey:"
+                                       + masterKey);
+               }
+               masterKey = masterKey.trim();
+               byte[] keyBytes = getUtf8Bytes(masterKey);
+               init(keyBytes);
+       }
+
+       /**
+        * @return UTF-8 byte array for the input string.
+        */
+       private static byte[] getUtf8Bytes(String s) {
+               if (s == null) {
+                       return ArrayUtils.EMPTY_BYTE_ARRAY;
+               }
+               ByteBuffer bb = UTF8_CHARSET.encode(s);
+               return ArrayUtils.subarray(bb.array(), 0, bb.limit());
+
+       }
+
+       private void init(byte[] masterKey) {
+               if (masterKey.length < MASTER_KEY_MIN_LEN) {
+                       // "Master key needs at least %s bytes", 
MASTER_KEY_MIN_LEN);
+               }
+
+               cipherKey = deriveKey(CIPHER_KEY_LABEL, masterKey, 16);
+               hmacKey = deriveKey(HMAC_KEY_LABEL, masterKey, 0);
+       }
+
+       /**
+        * Generates unique keys from a master key.
+        * 
+        * @param label
+        *            type of key to derive
+        * @param masterKey
+        *            master key
+        * @param len
+        *            length of key needed, less than 20 bytes. 20 bytes are
+        *            returned if len is 0.
+        * 
+        * @return a derived key of the specified length
+        */
+       private byte[] deriveKey(byte label, byte[] masterKey, int len) {
+               byte[] base = concat(new byte[] { label }, masterKey);
+               byte[] hash = DigestUtils.sha(base);
+               if (len == 0) {
+                       return hash;
+               }
+               byte[] out = new byte[len];
+               System.arraycopy(hash, 0, out, 0, out.length);
+               return out;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.apache.shindig.util.BlobCrypter#wrap(java.util.Map)
+        */
+       public String wrap(Map<String, String> in) throws Exception {
+               try {
+                       byte[] encoded = serialize(in);
+                       byte[] cipherText = aes128cbcEncrypt(cipherKey, 
encoded);
+                       byte[] hmac = hmacSha1(hmacKey, cipherText);
+                       byte[] b64 = 
Base64.encodeBase64URLSafe(concat(cipherText, hmac));
+                       return new String(b64, "UTF-8");
+               } catch (GeneralSecurityException e) {
+                       throw new Exception(e);
+               }
+       }
+
+       /**
+        * Encode the input for transfer. We use something a lot like HTML form
+        * encodings.
+        * 
+        * @param in
+        *            map of parameters to encode
+        */
+       private byte[] serialize(Map<String, String> in) {
+               StringBuilder sb = new StringBuilder();
+
+               for (Map.Entry<String, String> val : in.entrySet()) {
+                       sb.append(encode(val.getKey()));
+                       sb.append('=');
+                       sb.append(encode(val.getValue()));
+                       sb.append('&');
+               }
+               if (sb.length() > 0) {
+                       sb.deleteCharAt(sb.length() - 1); // Remove the last &
+               }
+               return getUtf8Bytes(sb.toString());
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.apache.shindig.util.BlobCrypter#unwrap(java.lang.String, 
int)
+        */
+       public Map<String, String> unwrap(String in) throws Exception {
+               try {
+                       byte[] bin = Base64.decodeBase64(getUtf8Bytes(in));
+                       byte[] hmac = new byte[HMAC_SHA1_LEN];
+                       byte[] cipherText = new byte[bin.length - 
HMAC_SHA1_LEN];
+                       System.arraycopy(bin, 0, cipherText, 0, 
cipherText.length);
+                       System.arraycopy(bin, cipherText.length, hmac, 0, 
hmac.length);
+                       hmacSha1Verify(hmacKey, cipherText, hmac);
+                       byte[] plain = aes128cbcDecrypt(cipherKey, cipherText);
+                       Map<String, String> out = deserialize(plain);
+                       return out;
+               } catch (GeneralSecurityException e) {
+                       throw new Exception("Invalid token signature", e);
+               } catch (ArrayIndexOutOfBoundsException e) {
+                       throw new Exception("Invalid token format", e);
+               } catch (NegativeArraySizeException e) {
+                       throw new Exception("Invalid token format", e);
+               }
+
+       }
+
+       private Map<String, String> deserialize(byte[] plain)
+                       throws UnsupportedEncodingException {
+               String base = new String(plain, "UTF-8");
+               // replaces [&=] regex
+               String[] items = StringUtils.splitPreserveAllTokens(base, "&=");
+               Map<String, String> map = new HashMap<String, String>();
+               for (int i = 0; i < items.length;) {
+                       String key = decode(items[i++]);
+                       String val = decode(items[i++]);
+                       map.put(key, val);
+               }
+               return map;
+       }
+
+       private static String encode(String input) {
+               try {
+                       return URLEncoder.encode(input, "UTF-8");
+               } catch (UnsupportedEncodingException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       private static String decode(String input) {
+               try {
+                       return URLDecoder.decode(input, "UTF-8");
+               } catch (UnsupportedEncodingException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       /**
+        * Returns the values from each provided array combined into a single 
array.
+        * For example, {@code concat(new byte[] a, b}, new byte[] {}, new 
byte[]
+        * {c}} returns the array {@code a, b, c}}.
+        * 
+        * @param arrays
+        *            zero or more {@code byte} arrays
+        * @return a single array containing all the values from the source 
arrays,
+        *         in order
+        */
+       private static byte[] concat(byte[]... arrays) {
+               int length = 0;
+               for (byte[] array : arrays) {
+                       length += array.length;
+               }
+               byte[] result = new byte[length];
+               int pos = 0;
+               for (byte[] array : arrays) {
+                       System.arraycopy(array, 0, result, pos, array.length);
+                       pos += array.length;
+               }
+               return result;
+       }
+
+       private static byte[] aes128cbcEncrypt(byte[] key, byte[] plain)
+                       throws GeneralSecurityException {
+               Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+               byte iv[] = getRandomBytes(cipher.getBlockSize());
+               return concat(iv, aes128cbcEncryptWithIV(key, iv, plain));
+       }
+
+       /**
+        * AES-128-CBC decryption. The IV is assumed to be the first 16 bytes 
of the
+        * cipher text.
+        * 
+        * @param key
+        * @param cipherText
+        * 
+        * @return the plain text
+        * 
+        * @throws GeneralSecurityException
+        */
+       private static byte[] aes128cbcDecrypt(byte[] key, byte[] cipherText)
+                       throws GeneralSecurityException {
+               byte iv[] = new byte[CIPHER_BLOCK_SIZE];
+               System.arraycopy(cipherText, 0, iv, 0, iv.length);
+               return aes128cbcDecryptWithIv(key, iv, cipherText, iv.length);
+       }
+
+       /**
+        * Returns strong random bytes.
+        * 
+        * @param numBytes
+        *            number of bytes of randomness
+        */
+       private static byte[] getRandomBytes(int numBytes) {
+               SecureRandom RAND = new SecureRandom();
+               byte[] out = new byte[numBytes];
+               RAND.nextBytes(out);
+               return out;
+       }
+
+       /**
+        * AES-128-CBC encryption with a given IV.
+        * 
+        * @param key
+        * @param iv
+        * @param plain
+        * 
+        * @return the cipher text
+        * 
+        * @throws GeneralSecurityException
+        */
+       private static byte[] aes128cbcEncryptWithIV(byte[] key, byte[] iv,
+                       byte[] plain) throws GeneralSecurityException {
+               Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+               Key cipherKey = new SecretKeySpec(key, CIPHER_KEY_TYPE);
+               IvParameterSpec ivSpec = new IvParameterSpec(iv);
+               cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
+               return cipher.doFinal(plain);
+       }
+
+       /**
+        * AES-128-CBC decryption with a particular IV.
+        * 
+        * @param key
+        *            decryption key
+        * @param iv
+        *            initial vector for decryption
+        * @param cipherText
+        *            cipher text to decrypt
+        * @param offset
+        *            offset into cipher text to begin decryption
+        * 
+        * @return the plain text
+        * 
+        * @throws GeneralSecurityException
+        */
+       private static byte[] aes128cbcDecryptWithIv(byte[] key, byte[] iv,
+                       byte[] cipherText, int offset) throws 
GeneralSecurityException {
+               Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+               Key cipherKey = new SecretKeySpec(key, CIPHER_KEY_TYPE);
+               IvParameterSpec ivSpec = new IvParameterSpec(iv);
+               cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
+               return cipher.doFinal(cipherText, offset, cipherText.length - 
offset);
+       }
+
+       /**
+        * HMAC sha1
+        * 
+        * @param key
+        *            the key must be at least 8 bytes in length.
+        * @param in
+        *            byte array to HMAC.
+        * @return the hash
+        * 
+        * @throws GeneralSecurityException
+        */
+       private static byte[] hmacSha1(byte[] key, byte[] in)
+                       throws GeneralSecurityException {
+               if (key.length < MIN_HMAC_KEY_LEN) {
+                       throw new GeneralSecurityException("HMAC key should be 
at least "
+                                       + MIN_HMAC_KEY_LEN + " bytes.");
+               }
+               Mac hmac = Mac.getInstance(HMAC_TYPE);
+               Key hmacKey = new SecretKeySpec(key, HMAC_TYPE);
+               hmac.init(hmacKey);
+               hmac.update(in);
+               return hmac.doFinal();
+       }
+
+       /**
+        * Verifies an HMAC SHA1 hash. Throws if the verification fails.
+        * 
+        * @param key
+        * @param in
+        * @param expected
+        * @throws GeneralSecurityException
+        */
+       private static void hmacSha1Verify(byte[] key, byte[] in, byte[] 
expected)
+                       throws GeneralSecurityException {
+               Mac hmac = Mac.getInstance(HMAC_TYPE);
+               Key hmacKey = new SecretKeySpec(key, HMAC_TYPE);
+               hmac.init(hmacKey);
+               hmac.update(in);
+               byte actual[] = hmac.doFinal();
+               if (actual.length != expected.length) {
+                       throw new GeneralSecurityException("HMAC verification 
failure");
+               }
+               for (int i = 0; i < actual.length; i++) {
+                       if (actual[i] != expected[i]) {
+                               throw new GeneralSecurityException("HMAC 
verification failure");
+                       }
+               }
+       }
+}

Added: 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenUtils.java
URL: 
http://svn.apache.org/viewvc/wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenUtils.java?rev=1564424&view=auto
==============================================================================
--- 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenUtils.java
 (added)
+++ 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/AuthTokenUtils.java
 Tue Feb  4 19:12:48 2014
@@ -0,0 +1,146 @@
+/*
+ * 
+ * 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.wookie.auth;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.wookie.server.security.ApiKey;
+import org.apache.wookie.server.security.ApiKeys;
+
+/**
+ * Utilities for token handling, including encrypting an AuthToken object into 
an opaque string,
+ * and decrypting a string token to retrieve an AuthToken object. 
+ */
+public class AuthTokenUtils {
+
+       private static final String WIDGET_ID_NAME = "widgetId";
+       private static final String VIEWER_ID_NAME = "viewerId";
+       private static final String CONTEXT_ID_NAME = "contextId";
+       private static final String API_KEY_HASH_NAME = "apiKey";
+       private static final String EXPIRES_NAME = "expires";
+       private static final String LANG_NAME = "lang"; 
+
+       /**
+        * Validate a token, and return an AuthToken object if its valid
+        * @param token
+        * @return a valid AuthToken object
+        * @throws InvalidAuthTokenException if the token is not valid
+        */
+       public static AuthToken validateAuthToken(String token) throws 
InvalidAuthTokenException{
+               AuthToken authToken = 
createAuthToken(extractParametersFromToken(token));
+               if (authToken.isExpired()){
+                       throw new 
InvalidAuthTokenException(InvalidAuthTokenException.EXPIRED);
+               } else {
+                       return authToken;
+               }
+       }
+       
+       /**
+        * Get a widget instance for the given token. Attempt to decrypt the
+        * token
+        * @param token
+        * @return an AuthToken, or null if the token is not valid
+        * @throws Exception 
+        */
+       public static AuthToken decryptAuthToken(String token) throws 
InvalidAuthTokenException{
+               return createAuthToken(extractParametersFromToken(token));
+       }
+
+       /*
+        * Decrypts a token and extracts the contents as a Map of Strings.
+        * @param token the token to decrpyt
+        * @return a Map of key-value pairs extracted from the token
+        * @throws Exception
+        */
+       private static Map<String, String> extractParametersFromToken(String 
token) throws InvalidAuthTokenException{
+               try {
+                       AuthTokenCrypter crypter = new AuthTokenCrypter();
+                       return crypter.unwrap(token);
+               } catch (Exception e) {
+                       throw new 
InvalidAuthTokenException(InvalidAuthTokenException.INVALID_ENCRYPTION);
+               }
+       }
+
+       /*
+        * Create an AuthToken from the given set of parameters
+        * @param parameters the parameters to use to construct the AuthToken
+        * @return a new AuthToken
+        * @throws Exception 
+        */
+       private static AuthToken createAuthToken(Map<String, String> 
parameters) throws InvalidAuthTokenException{
+               AuthToken authToken = new AuthToken();
+               authToken.setWidgetId(parameters.get(WIDGET_ID_NAME));
+               authToken.setContextId(parameters.get(CONTEXT_ID_NAME));
+               authToken.setViewerId(parameters.get(VIEWER_ID_NAME));
+               authToken.setLang(parameters.get(LANG_NAME));
+               if (parameters.get(EXPIRES_NAME) != null){
+                       
authToken.setExpiresAt(Long.valueOf(parameters.get(EXPIRES_NAME)));
+               } else {
+                       authToken.setExpires();
+               }
+               ApiKey apiKey = 
getApiKeyFromHash(Integer.valueOf(parameters.get(API_KEY_HASH_NAME)));
+               if (apiKey == null) throw new 
InvalidAuthTokenException(InvalidAuthTokenException.INVALID_CONTENT);
+               authToken.setApiKey(apiKey);
+               return authToken;
+       }
+
+       /*
+        * Check that the hash supplied matches an API key, and
+        * return the matching key
+        * @param hashcode
+        * @return the API key corresponding to the hashcode
+        */
+       private static ApiKey getApiKeyFromHash(int hashcode){
+               for (ApiKey apiKey: ApiKeys.getInstance().getKeys()){
+                       if (apiKey.getValue().hashCode() == hashcode) return 
apiKey;
+               }
+               return null;
+       }
+
+       /**
+        * Encrypts a token
+        * @param authToken
+        * @return the encrypted token
+        * @throws Exception 
+        */
+       public static String encryptAuthToken(AuthToken authToken) throws 
Exception{
+               Map<String, String> parameters = new HashMap<String, String>();
+               parameters.put(WIDGET_ID_NAME, authToken.getWidgetId());
+               parameters.put(CONTEXT_ID_NAME, authToken.getContextId());
+               parameters.put(VIEWER_ID_NAME, authToken.getViewerId());
+               parameters.put(API_KEY_HASH_NAME, 
String.valueOf(authToken.getApiKey().hashCode()));
+               parameters.put(LANG_NAME, authToken.getLang());
+               if (authToken.getExpiresAt() != null){
+                       parameters.put(EXPIRES_NAME, 
String.valueOf(authToken.getExpiresAt()));
+               }
+               return encrypt(parameters);             
+       }
+
+       /*
+        * Encrypts a token with the given key
+        * @param parameters the set of parameters to encrypt
+        * @return the encrypted token
+        * @throws Exception
+        */
+       private static String encrypt(Map<String, String> parameters) throws 
Exception{
+               AuthTokenCrypter crypter = new AuthTokenCrypter();
+               return crypter.wrap(parameters);
+       }
+
+}

Added: 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/InvalidAuthTokenException.java
URL: 
http://svn.apache.org/viewvc/wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/InvalidAuthTokenException.java?rev=1564424&view=auto
==============================================================================
--- 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/InvalidAuthTokenException.java
 (added)
+++ 
wookie/trunk/wookie-server/src/main/java/org/apache/wookie/auth/InvalidAuthTokenException.java
 Tue Feb  4 19:12:48 2014
@@ -0,0 +1,49 @@
+/*
+ * 
+ * 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.wookie.auth;
+
+/**
+ * Exception thrown when an Auth Token supplied by a widget
+ * is invalid, for example where it is incorrectly encrypted,
+ * where the content is incorrect (e.g. no API key) or if
+ * the token has expired.
+ *
+ */
+public class InvalidAuthTokenException extends Exception{
+
+       public static final int EXPIRED = 0;
+       public static final int INVALID_CONTENT = 1;
+       public static final int INVALID_ENCRYPTION = 1; 
+       
+       private int reason;
+       
+       public InvalidAuthTokenException(int reason){
+               this.reason = reason;
+       }
+       
+       /**
+        * Get the reason why the token is invalid
+        * @return
+        */
+       public int getReason(){
+               return reason;
+       }
+       
+       private static final long serialVersionUID = -2998867462005878209L;     
+       
+}

Added: 
wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/AuthTokenUtilsTest.java
URL: 
http://svn.apache.org/viewvc/wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/AuthTokenUtilsTest.java?rev=1564424&view=auto
==============================================================================
--- 
wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/AuthTokenUtilsTest.java
 (added)
+++ 
wookie/trunk/wookie-server/src/test/java/org/apache/wookie/auth/AuthTokenUtilsTest.java
 Tue Feb  4 19:12:48 2014
@@ -0,0 +1,236 @@
+/*
+ * 
+ * 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.wookie.auth;
+
+import static org.junit.Assert.*;
+
+import org.apache.wookie.auth.AuthToken;
+import org.apache.wookie.auth.AuthTokenUtils;
+import org.apache.wookie.exceptions.ResourceDuplicationException;
+import org.apache.wookie.exceptions.ResourceNotFoundException;
+import org.apache.wookie.server.security.ApiKey;
+import org.apache.wookie.server.security.ApiKeys;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests creating, parsing and validating auth tokens
+ */
+public class AuthTokenUtilsTest{
+
+       @BeforeClass
+       public static void setup() throws ResourceDuplicationException{
+               ApiKeys.getInstance().addKey("ENC_TEST", "[email protected]");
+       }
+
+       @Test
+       public void encrypt() throws Exception{
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               
+               AuthToken decryptedToken = 
AuthTokenUtils.decryptAuthToken(token);
+
+               assertEquals("shared", decryptedToken.getContextId());
+               assertEquals("user1", decryptedToken.getViewerId());
+               assertEquals("http://apache.org/widgets/test";, 
decryptedToken.getWidgetId());
+               assertEquals("ENC_TEST", decryptedToken.getApiKey());
+               assertFalse(decryptedToken.isExpired());
+       }
+       
+       @Test
+       public void validate() throws Exception{
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               
+               AuthToken decryptedToken = 
AuthTokenUtils.validateAuthToken(token);
+
+               assertEquals("shared", decryptedToken.getContextId());
+               assertEquals("user1", decryptedToken.getViewerId());
+               assertEquals("http://apache.org/widgets/test";, 
decryptedToken.getWidgetId());
+               assertEquals("ENC_TEST", decryptedToken.getApiKey());
+               assertFalse(decryptedToken.isExpired());
+       }
+       
+       @Test
+       public void encryptExpiry() throws Exception{
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+               // Set expiry to ten minutes ago
+               authToken.setExpiresAt(System.currentTimeMillis() / 1000 - 600);
+
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               AuthToken decryptedToken = 
AuthTokenUtils.decryptAuthToken(token);
+
+               assertEquals("shared", decryptedToken.getContextId());
+               assertEquals("user1", decryptedToken.getViewerId());
+               assertEquals("http://apache.org/widgets/test";, 
decryptedToken.getWidgetId());
+               assertEquals("ENC_TEST", decryptedToken.getApiKey());
+               assertTrue(decryptedToken.isExpired());
+       }
+       
+       
+       /**
+        * Use validation method instead of decrypt with an expired token
+        * @throws Exception
+        */
+       @Test
+       public void encryptExpiryWithValidation() throws Exception{
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+               // Set expiry to ten minutes ago
+               authToken.setExpiresAt(System.currentTimeMillis() / 1000 - 600);
+
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               
+               AuthToken decryptedToken = null;
+               int errorCode = -1;
+               try {
+                       decryptedToken = 
AuthTokenUtils.validateAuthToken(token);
+               } catch (InvalidAuthTokenException e) {
+                       errorCode = e.getReason();
+               }
+               
+
+               assertEquals(InvalidAuthTokenException.EXPIRED, errorCode);
+               assertNull(decryptedToken);
+       }
+       
+       @Test
+       public void encryptExpirySkew() throws Exception{
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+               // Set expiry to two minutes ago; this will still be valid
+               // using the default clock skew of 3 minutes.
+               authToken.setExpiresAt(System.currentTimeMillis() / 1000 - 120);
+
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               
+               AuthToken decryptedToken = 
AuthTokenUtils.decryptAuthToken(token);
+
+               assertEquals("shared", decryptedToken.getContextId());
+               assertEquals("user1", decryptedToken.getViewerId());
+               assertEquals("http://apache.org/widgets/test";, 
decryptedToken.getWidgetId());
+               assertEquals("ENC_TEST", decryptedToken.getApiKey());
+               assertFalse(decryptedToken.isExpired());
+       }
+       
+       @Test
+       public void encryptBadApiKey(){
+               ApiKey apiKey = new ApiKey();
+               apiKey.setValue("ENC_TEST_BAD");
+               apiKey.setEmail("[email protected]");
+
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(apiKey);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               authToken.setLang("en");
+
+               String token = "";
+               try {
+                       token = AuthTokenUtils.encryptAuthToken(authToken);
+               } catch (Exception e) {
+                       fail();
+               }
+
+               int errorCode = -1;
+               try {
+                       AuthTokenUtils.decryptAuthToken(token);
+               } catch (InvalidAuthTokenException e) {
+                       errorCode = e.getReason();
+               }
+               
+               assertEquals(InvalidAuthTokenException.INVALID_CONTENT, 
errorCode);
+       }
+       
+       @Test(expected=Exception.class)
+       public void encryptNullApiKey() throws Exception{
+               AuthToken authToken = new AuthToken();
+               authToken.setApiKey(null);
+               authToken.setContextId("shared");
+               authToken.setViewerId("user1");
+               authToken.setWidgetId("http://apache.org/widgets/test";);
+               String token = AuthTokenUtils.encryptAuthToken(authToken);
+               AuthTokenUtils.decryptAuthToken(token);
+       }
+       
+       @Test
+       public void noEncrypt(){
+               String token = "not a real token";
+               int errorCode = -1;
+               try {
+                       AuthTokenUtils.decryptAuthToken(token);
+               } catch (InvalidAuthTokenException e) {
+                       errorCode = e.getReason();
+               }
+               assertEquals(InvalidAuthTokenException.INVALID_ENCRYPTION, 
errorCode);
+       }
+
+       @AfterClass
+       public static void tearDown() throws ResourceNotFoundException{
+               ApiKeys.getInstance().removeKey("ENC_TEST");
+       }
+
+
+}


Reply via email to