Skip to content

Commit bea2e23

Browse files
author
Amir Tocker
committed
Add URL authorization token. Rename token function.
1 parent 93a839e commit bea2e23

File tree

7 files changed

+134
-66
lines changed

7 files changed

+134
-66
lines changed

lib/utils.js

Lines changed: 15 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/utils.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth_token.coffee

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
###*
2+
* Authorization Token
3+
* @module auth_token
4+
###
5+
crypto = require('crypto')
6+
config = require('./config')
7+
8+
digest = (message, key) ->
9+
crypto.createHmac("sha256", new Buffer(key, "hex"))
10+
.update message
11+
.digest 'hex'
12+
13+
###*
14+
* Generate an authorization token
15+
* @param {Object} options
16+
* @param {string} options.key - the secret key required to sign the token
17+
* @param {string} [options.ip] - the IP address of the client
18+
* @param {number} [options.start_time=now] - the start time of the token in seconds from epoch
19+
* @param {string} [options.expiration] - the expiration time of the token in seconds from epoch
20+
* @param {string} [options.duration] - the duration of the token (from start_time)
21+
* @param {string} [options.acl] - the ACL for the token
22+
* @param {string} [options.url] - the URL to authentication in case of a URL token
23+
* @returns {string} the authorization token
24+
###
25+
module.exports = (options)->
26+
params = Object.assign {}, config().auth_token, options
27+
tokenName = params.token_name ? "__cld_token__"
28+
29+
unless params.expiration?
30+
if params.duration?
31+
start = params.start_time ? Math.round(Date.now() / 1000)
32+
params.expiration = start + params.duration
33+
else
34+
throw new Error( "Must provide either end_time or window")
35+
36+
tokenParts = []
37+
tokenParts.push("ip=#{params.ip}") if params.ip?
38+
tokenParts.push("st=#{params.start_time}") if params.start_time?
39+
tokenParts.push("exp=#{params.expiration}")
40+
tokenParts.push("acl=#{params.acl}") if params.acl?
41+
toSign = (part for part in tokenParts)
42+
if params.url
43+
url = encodeURIComponent(params.url).replace(/%../g, (match)-> match.toLowerCase())
44+
toSign.push "url=#{url}"
45+
auth = digest(toSign.join("~"), params.key)
46+
tokenParts.push("hmac=#{auth}")
47+
"#{tokenName}=#{tokenParts.join('~')}"

src/generateAkamaiToken.coffee

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/utils.coffee

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ _ = require("lodash")
22
config = require("./config")
33
crypto = require('crypto')
44
querystring = require('querystring')
5+
url = require('url')
6+
57
utils = exports
6-
exports.generateAkamaiToken = require("./generateAkamaiToken")
8+
exports.generate_auth_token = require("./auth_token")
79
exports.CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"
810
exports.OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"
911
exports.AKAMAI_SHARED_CDN = "res.cloudinary.com"
@@ -388,6 +390,7 @@ exports.url = (public_id, options = {}) ->
388390
api_secret = utils.option_consume(options, "api_secret", config().api_secret)
389391
url_suffix = utils.option_consume(options, "url_suffix")
390392
use_root_path = utils.option_consume(options, "use_root_path", config().use_root_path)
393+
auth_token = utils.option_consume(options, "auth_token", config().auth_token)
391394

392395
preloaded = /^(image|raw)\/([a-z0-9_]+)\/v(\d+)\/([^#]+)$/.exec(public_id)
393396
if preloaded
@@ -415,7 +418,7 @@ exports.url = (public_id, options = {}) ->
415418
version = "v#{version}" if version?
416419

417420
transformation = transformation.replace(/([^:])\/\//g, '$1/')
418-
if sign_url
421+
if sign_url && !auth_token
419422
to_sign = [transformation, source_to_sign].filter((part) -> part? && part != '').join('/')
420423
shasum = crypto.createHash('sha1')
421424
shasum.update(utf8_encode(to_sign + api_secret), 'binary')
@@ -424,9 +427,12 @@ exports.url = (public_id, options = {}) ->
424427

425428

426429
prefix = unsigned_url_prefix(public_id, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
427-
url = [prefix, resource_type, type, signature, transformation, version,
430+
resultUrl = [prefix, resource_type, type, signature, transformation, version,
428431
public_id].filter((part) -> part? && part != '').join('/')
429-
url
432+
if sign_url && auth_token
433+
token = utils.generate_auth_token exports.merge(url: url.parse(resultUrl).path, auth_token)
434+
resultUrl += "?#{token}"
435+
resultUrl
430436

431437
exports.video_url = (public_id, options) ->
432438
options = _.extend({resource_type: 'video'}, options)

test/authtoken_spec.coffee

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
expect = require("expect.js")
2+
cloudinary = require("../cloudinary.js")
3+
utils = cloudinary.utils
4+
5+
KEY = "00112233FF99"
6+
ALT_KEY = "CCBB2233FF00"
7+
describe "authToken", ->
8+
urlBackup = process.env.CLOUDINARY_URL
9+
10+
beforeEach ->
11+
process.env.CLOUDINARY_URL = "cloudinary://a:b@test123?load_strategies=false"
12+
cloudinary.config true
13+
cloudinary.config().auth_token = {key: KEY, duration: 300, start_time: 11111111}
14+
after ->
15+
process.env.CLOUDINARY_URL = urlBackup
16+
cloudinary.config true
17+
18+
it "should generate with start and window", ->
19+
token = utils.generate_auth_token start_time: 1111111111, acl: "/image/*", duration: 300
20+
expect(token).to.eql '__cld_token__=st=1111111111~exp=1111111411~acl=/image/*~hmac=0854e8b6b6a46471a80b2dc28c69bd352d977a67d031755cc6f3486c121b43af'
21+
22+
describe "authenticated url", ->
23+
beforeEach ->
24+
cloudinary.config private_cdn: true
25+
26+
it "should add token if authToken is globally set and signed = true", ->
27+
url = cloudinary.url "sample.jpg", sign_url: true, resource_type: "image", type: "authenticated", version: "1486020273"
28+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3")
29+
30+
it "should add token for 'public' resource", ->
31+
url = cloudinary.url "sample.jpg", sign_url: true, resource_type: "image", type: "public", version: "1486020273"
32+
expect(url).to.eql("http://test123-res.cloudinary.com/image/public/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=c2b77d9f81be6d89b5d0ebc67b671557e88a40bcf03dd4a6997ff4b994ceb80e")
33+
34+
it "should not add token if signed is false", ->
35+
url = cloudinary.url "sample.jpg", type: "authenticated", version: "1486020273"
36+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg")
37+
38+
it "should not add token if authToken is globally set but null auth token is explicitly set and signed = true", ->
39+
url = cloudinary.url "sample.jpg", auth_token: false, sign_url: true, type: "authenticated", version: "1486020273"
40+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/s--v2fTPYTu--/v1486020273/sample.jpg")
41+
42+
it "explicit authToken should override global setting", ->
43+
url = cloudinary.url "sample.jpg", sign_url: true, auth_token: {key: ALT_KEY, start_time: 222222222, duration: 100}, type: "authenticated", transformation: {crop: "scale", width: 300}
44+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=7d276841d70c4ecbd0708275cd6a82e1f08e47838fbb0bceb2538e06ddfa3029")
45+
46+
it "should compute expiration as start time + duration", ->
47+
token = {key: KEY, start_time: 11111111, duration: 300}
48+
url = cloudinary.url "sample.jpg", sign_url: true, auth_token: token, resource_type: "image", type: "authenticated", version: "1486020273"
49+
expect(url).to.eql("http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3")
50+
51+
describe "authentication token", ->
52+
it "should generate token string", ->
53+
user = "foobar" # we can't rely on the default "now" value in tests
54+
tokenOptions = {key: KEY, duration: 300, acl: "/*/t_#{user}"}
55+
tokenOptions.start_time = 222222222 # we can't rely on the default "now" value in tests
56+
cookieToken = utils.generate_auth_token tokenOptions
57+
expect(cookieToken).to.eql("__cld_token__=st=222222222~exp=222222522~acl=/*/t_foobar~hmac=eb5e2266c8ec9573f696025f075b92998080347e1c12ac39a26c94d7d712704a")
58+
59+
it "should add token to an image tag url", ->
60+
tag = cloudinary.image "sample.jpg", sign_url: true, type: "authenticated", version: "1486020273"
61+
expect(tag).to.match /<img.*src='http:\/\/res.cloudinary.com\/test123\/image\/authenticated\/v1486020273\/sample.jpg\?__cld_token__=st=11111111~exp=11111411~hmac=9bd6f41e2a5893da8343dc8eb648de8bf73771993a6d1457d49851250caf3b80.*>/

test/utils_spec.coffee

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -573,30 +573,3 @@ describe "utils", ->
573573
"if": "w = 0 && height != 0 || aspectRatio < 0 and pageCount > 0 and faceCount <= 0 and width >= 0",
574574
"effect": "grayscale",
575575
)).to.match(new RegExp(allOperators))
576-
577-
describe 'generateAkamaiToken', ->
578-
beforeEach ->
579-
cloudinary.config( akamai_key: '00112233FF99')
580-
afterEach ->
581-
cloudinary.config(true)
582-
583-
it "should generate an Akamai token with start_time and window", ->
584-
token = utils.generateAkamaiToken start_time: 1111111111, acl: '/image/*', window: 300
585-
expect(token).to.eql('__cld_token__=st=1111111111~exp=1111111411~acl=/image/*~hmac=0854e8b6b6a46471a80b2dc28c69bd352d977a67d031755cc6f3486c121b43af')
586-
it "should generate an Akamai token with window", ->
587-
first_exp = Math.round(Date.now() / 1000 )+ 300
588-
# expiration is calculated automatically as now + window
589-
token = utils.generateAkamaiToken acl: '*', window: 300
590-
second_exp = Math.round(Date.now() / 1000 )+ 300
591-
match = /exp=(\d+)/.exec(token)
592-
expect(match[1]).to.be.ok()
593-
expiration = parseInt(match[1])
594-
expect(expiration).to.be.within(first_exp, second_exp)
595-
expect(utils.generateAkamaiToken acl: '*', end_time: expiration).to.eql(token)
596-
597-
it "should accept a key", ->
598-
expect(utils.generateAkamaiToken acl: '*', end_time: 10000000, key: '00aabbff')
599-
.to.eql('__cld_token__=exp=10000000~acl=*~hmac=030eafb6b19e499659d699b3d43e7595e35e3c0060e8a71904b3b8c8759f4890')
600-
601-
it "should throw if no end_time or window is provided", ->
602-
expect( -> utils.generateAkamaiToken( acl: '*') ).to.throwError()

0 commit comments

Comments
 (0)