Skip to content

Commit d45a0ff

Browse files
author
Amir Tocker
committed
Add Akamai token generator
1 parent 83e7a96 commit d45a0ff

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.cloudinary;
2+
3+
import com.cloudinary.utils.StringUtils;
4+
5+
import javax.crypto.Mac;
6+
import javax.crypto.spec.SecretKeySpec;
7+
import javax.xml.bind.DatatypeConverter;
8+
import java.security.InvalidKeyException;
9+
import java.security.NoSuchAlgorithmException;
10+
import java.util.ArrayList;
11+
import java.util.Calendar;
12+
import java.util.TimeZone;
13+
14+
/**
15+
* Token generator for Akamai authentication
16+
*/
17+
public class AkamaiToken {
18+
public String tokenName = Cloudinary.AKAMAI_TOKEN_NAME;
19+
public String key;
20+
public long startTime;
21+
public long endTime;
22+
public String ip;
23+
public String acl;
24+
public long window;
25+
26+
public AkamaiToken(String key) {
27+
this.key = key;
28+
}
29+
30+
public AkamaiToken setTokenName(String tokenName) {
31+
this.tokenName = tokenName;
32+
return this;
33+
}
34+
35+
/**
36+
* Set the start time of the token. Defaults to now.
37+
*
38+
* @param startTime in seconds since epoch
39+
* @return
40+
*/
41+
public AkamaiToken setStartTime(long startTime) {
42+
this.startTime = startTime;
43+
return this;
44+
}
45+
46+
/**
47+
* Set the end time (expiration) of the token
48+
*
49+
* @param endTime in seconds since epoch
50+
* @return
51+
*/
52+
public AkamaiToken setEndTime(long endTime) {
53+
this.endTime = endTime;
54+
return this;
55+
}
56+
57+
public AkamaiToken setIp(String ip) {
58+
this.ip = ip;
59+
return this;
60+
}
61+
62+
public AkamaiToken setAcl(String acl) {
63+
this.acl = acl;
64+
return this;
65+
}
66+
67+
/**
68+
* The duration of the token in seconds. This value is used to calculate the expiration of the token.
69+
* It is ignored if endTime is provided.
70+
*
71+
* @param window
72+
* @return
73+
*/
74+
public AkamaiToken setWindow(long window) {
75+
this.window = window;
76+
return this;
77+
}
78+
79+
/**
80+
* Generate the authentication token
81+
*
82+
* @return a signed token
83+
*/
84+
public String generate() {
85+
long expiration = endTime;
86+
if (expiration == 0) {
87+
if (window > 0) {
88+
final long start = startTime > 0 ? startTime : Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L;
89+
expiration = start + window;
90+
} else {
91+
throw new IllegalArgumentException("Must provide either endTime or window");
92+
}
93+
}
94+
ArrayList<String> tokenParts = new ArrayList<String>();
95+
if (ip != null) {
96+
tokenParts.add("ip=" + ip);
97+
}
98+
if (startTime > 0) {
99+
tokenParts.add("st=" + startTime);
100+
}
101+
tokenParts.add("exp=" + expiration);
102+
tokenParts.add("acl=" + acl);
103+
String auth = digest(StringUtils.join(tokenParts, "~"));
104+
tokenParts.add("hmac=" + auth);
105+
return tokenName + "=" + StringUtils.join(tokenParts, "~");
106+
}
107+
108+
109+
private String digest(String message) {
110+
byte[] binKey = DatatypeConverter.parseHexBinary(key);
111+
try {
112+
Mac hmac = Mac.getInstance("HmacSHA256");
113+
SecretKeySpec secret = new SecretKeySpec(binKey, "HmacSHA256");
114+
hmac.init(secret);
115+
final byte[] bytes = message.getBytes();
116+
return DatatypeConverter.printHexBinary(hmac.doFinal(bytes)).toLowerCase();
117+
} catch (NoSuchAlgorithmException e) {
118+
e.printStackTrace();
119+
} catch (InvalidKeyException e) {
120+
e.printStackTrace();
121+
}
122+
return null;
123+
}
124+
125+
126+
}

cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@SuppressWarnings({"rawtypes", "unchecked"})
2525
public class Cloudinary {
2626

27+
public static final String AKAMAI_TOKEN_NAME = "__cld_token__";
2728
private static List<String> UPLOAD_STRATEGIES = new ArrayList<String>(Arrays.asList(
2829
"com.cloudinary.android.UploaderStrategy",
2930
"com.cloudinary.http42.UploaderStrategy",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.cloudinary;
2+
3+
import org.hamcrest.Matchers;
4+
import org.junit.Test;
5+
6+
import java.util.Calendar;
7+
import java.util.TimeZone;
8+
import java.util.regex.Matcher;
9+
import java.util.regex.Pattern;
10+
11+
import static org.junit.Assert.*;
12+
13+
public class AkamaiTokenTest {
14+
public static final String KEY = "00112233FF99";
15+
16+
@Test
17+
public void generateWithStartAndWindow() throws Exception {
18+
AkamaiToken t = new AkamaiToken(KEY);
19+
t.setStartTime(1111111111).setAcl("/image/*").setWindow(300);
20+
assertEquals("should generate an Akamai token with startTime and window", "__cld_token__=st=1111111111~exp=1111111411~acl=/image/*~hmac=0854e8b6b6a46471a80b2dc28c69bd352d977a67d031755cc6f3486c121b43af", t.generate());
21+
}
22+
23+
@Test
24+
public void generateWithWindow() throws Exception {
25+
long firstExp = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L + 300;
26+
Thread.sleep(1200);
27+
String token = new AkamaiToken(KEY).setAcl("*").setWindow(300).generate();
28+
Thread.sleep(1200);
29+
long secondExp = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L + 300;
30+
Matcher m = Pattern.compile("exp=(\\d+)").matcher(token);
31+
assertTrue(m.find());
32+
final String expString = m.group(1);
33+
final long actual = Long.parseLong(expString);
34+
assertThat(actual, Matchers.greaterThanOrEqualTo(firstExp));
35+
assertThat(actual, Matchers.lessThanOrEqualTo(secondExp));
36+
assertEquals(token, new AkamaiToken(KEY).setAcl("*").setEndTime(actual).generate());
37+
}
38+
39+
@Test(expected = IllegalArgumentException.class)
40+
public void testMustProvideEndTimeOrWindow(){
41+
new AkamaiToken(KEY).setAcl("*").generate();
42+
}
43+
44+
45+
}

0 commit comments

Comments
 (0)