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");
+ }
+
+
+}