Skip to content

Commit

Permalink
Rename auth_token. Add URL authentication.
Browse files Browse the repository at this point in the history
  • Loading branch information
Amir Tocker committed Feb 21, 2017
1 parent 6923496 commit ded9dc1
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 105 deletions.
51 changes: 0 additions & 51 deletions src/Akamai.php

This file was deleted.

72 changes: 72 additions & 0 deletions src/AuthToken.php
@@ -0,0 +1,72 @@
<?php

namespace Cloudinary;


class AuthToken {

/**
* Generate an authorization token.
* Options:
* string key - the secret key required to sign the token
* string ip - the IP address of the client
* number start_time - the start time of the token in seconds from epoch
* string expiration - the expiration time of the token in seconds from epoch
* string duration - the duration of the token (from start_time)
* string acl - the ACL for the token
* string url - the URL to authentication in case of a URL token
*
* @param array $options token configuration
*
* @return string the authorization token
* @throws Error if both expiration and duration were not provided
*/
public static function generate($options=array()){
$key = \Cloudinary::option_get($options, "key");
if(!isset($key)) throw new \Cloudinary\Error("Missing authentication token key configuration");
$name = \Cloudinary::option_get($options, "token_name", "__cld_token__");
$start = \Cloudinary::option_get($options, "start_time");
$expiration = \Cloudinary::option_get($options, "expiration");
$ip = \Cloudinary::option_get($options, "ip");
$acl = \Cloudinary::option_get($options, "acl");
$url = \Cloudinary::option_get($options, "url");
$duration = \Cloudinary::option_get($options, "duration");

if(!strcasecmp($start, "now")) {
$start = time();
} elseif (is_numeric($start)) {
$start = 0 + $start;
}
if(!isset($expiration)){
if(isset($duration)){
$expiration = (isset($start) ? $start : time()) + $duration;
} else {
throw new \Cloudinary\Error("Must provide 'expiration' or 'duration'.");
}
}
$token = array();
if(isset($ip)) array_push($token, "ip=$ip");
if(isset($start)) array_push($token, "st=$start");
array_push($token, "exp=$expiration");
if(isset($acl)) array_push($token, "acl=" . self::escape_to_lower($acl));
$to_sign = $token;
if(isset($url)) array_push($to_sign, "url=" . self::escape_to_lower($url));
$auth = self::digest(join("~", $to_sign), $key);
array_push($token, "hmac=$auth");
return "$name=" . join("~", $token);
}

private static function digest($message, $key = NULL) {
if(!isset($key)) $key = \Cloudinary::config_get("akamai_key");
$bin_key = pack("H*", $key);
return hash_hmac( "sha256", $message, $bin_key);
}

private static function escape_to_lower($url) {
$escaped_url = rawurlencode( $url );
$escaped_url = preg_replace_callback("/(%..)/", function($match) {
return strtolower($match[1]);
}, $escaped_url);
return $escaped_url;
}
}
46 changes: 41 additions & 5 deletions src/Cloudinary.php
@@ -1,9 +1,8 @@
<?php
require_once 'Akamai.php';
require_once 'AuthToken.php';

class Cloudinary {

use \Cloudinary\Akamai;
const CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net";
const OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net";
const AKAMAI_SHARED_CDN = "res.cloudinary.com";
Expand Down Expand Up @@ -495,6 +494,12 @@ public static function cloudinary_url($source, &$options=array()) {
$api_secret = Cloudinary::option_consume($options, "api_secret", Cloudinary::config_get("api_secret"));
$url_suffix = Cloudinary::option_consume($options, "url_suffix", Cloudinary::config_get("url_suffix"));
$use_root_path = Cloudinary::option_consume($options, "use_root_path", Cloudinary::config_get("use_root_path"));
$auth_token = Cloudinary::option_consume($options, "auth_token");
if (is_array($auth_token) ) {
$auth_token = array_merge(self::config_get("auth_token", array()), $auth_token);
} elseif (is_null($auth_token)) {
$auth_token = self::config_get("auth_token");
}

if (!$private_cdn and !empty($url_suffix)) {
throw new InvalidArgumentException("URL Suffix only supported in private CDN");
Expand All @@ -517,7 +522,7 @@ public static function cloudinary_url($source, &$options=array()) {
$version = $version ? "v" . $version : NULL;

$signature = NULL;
if ($sign_url) {
if ($sign_url && !$auth_token) {
$to_sign = implode("/", array_filter(array($transformation, $source_to_sign)));
$signature = str_replace(array('+','/','='), array('-','_',''), base64_encode(sha1($to_sign . $api_secret, TRUE)));
$signature = 's--' . substr($signature, 0, 8) . '--';
Expand All @@ -526,8 +531,21 @@ public static function cloudinary_url($source, &$options=array()) {
$prefix = Cloudinary::unsigned_download_url_prefix($source, $cloud_name, $private_cdn, $cdn_subdomain, $secure_cdn_subdomain,
$cname, $secure, $secure_distribution);

return preg_replace("/([^:])\/+/", "$1/", implode("/", array_filter(array($prefix, $resource_type_and_type,
$signature, $transformation, $version, $source))));
$source = preg_replace( "/([^:])\/+/", "$1/", implode( "/", array_filter( array(
$prefix,
$resource_type_and_type,
$signature,
$transformation,
$version,
$source
) ) ) );

if( $sign_url && $auth_token) {
$path = parse_url($source, PHP_URL_PATH);
$token = \Cloudinary\AuthToken::generate(array_merge($auth_token, array( "url" => $path)));
$source = $source . "?" . $token;
}
return $source;
}

private static function finalize_source($source, $format, $url_suffix) {
Expand Down Expand Up @@ -734,6 +752,24 @@ public static function download_zip_url($options=array()) {
return Cloudinary::download_archive_url($options);
}

/**
* Generate an authorization token.
* Options:
* string key - the secret key required to sign the token
* string ip - the IP address of the client
* number start_time - the start time of the token in seconds from epoch
* string expiration - the expiration time of the token in seconds from epoch
* string duration - the duration of the token (from start_time)
* string acl - the ACL for the token
* string url - the URL to authentication in case of a URL token
*
* @param array $options token configuration, merge with the global configuration "auth_token".
* @return string the authorization token
*/
public static function generate_auth_token($options){
$token_options = array_merge(self::config_get("auth_token", array()), $options);
return \Cloudinary\AuthToken::generate($token_options);
}

# Returns a Hash of parameters used to create an archive
# @param [Hash] options
Expand Down
137 changes: 137 additions & 0 deletions tests/AuthTokenTest.php
@@ -0,0 +1,137 @@
<?php
use Cloudinary\AuthToken;

$base = realpath( dirname( __FILE__ ) . DIRECTORY_SEPARATOR . '..' );
require_once( join( DIRECTORY_SEPARATOR, array( $base, 'src', 'Cloudinary.php' ) ) );

class AuthTokenTest extends PHPUnit_Framework_TestCase {
const KEY = "00112233FF99";
const ALT_KEY = "CCBB2233FF00";

private $url_backup;

protected function setUp() {
parent::setUp();
$this->url_backup = getenv( "CLOUDINARY_URL" );
\Cloudinary::config_from_url( "cloudinary://a:b@test123?auth_token[duration]=300&auth_token[start_time]=11111111&auth_token[key]=" . AuthTokenTest::KEY );
\Cloudinary::config( array( "private_cdn" => TRUE ) );
}

protected function tearDown() {
parent::tearDown();
putenv( "CLOUDINARY_URL=" . $this->url_backup );
\Cloudinary::config_from_url( $this->url_backup );
}

function test_generate_with_start_time_and_duration() {
$message = "should generate with start and duration";
$token = \Cloudinary::generate_auth_token( array(
"start_time" => 1111111111,
"acl" => "/image/*",
"duration" => 300
) );
$this->assertEquals( '__cld_token__=st=1111111111~exp=1111111411~acl=%2fimage%2f%2a~hmac=0d5b0c9c1485ee162c459879fe62e06caa23bc26fec92d58bd100f2e1592eac6', $token, $message );
}

function test_should_add_token_if_authToken_is_globally_set_and_signed_is_True() {
$message = "should add token if authToken is globally set and signed = true";
$options = array(
"sign_url" => TRUE,
"resource_type" => "image",
"type" => "authenticated",
"version" => "1486020273"
);
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3", $url, $message );


}

function test_should_add_token_for_public_resource() {
$message = "should add token for 'public' resource";
$options = array(
"sign_url" => TRUE,
"resource_type" => "image",
"type" => "public",
"version" => "1486020273"
);
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/public/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=c2b77d9f81be6d89b5d0ebc67b671557e88a40bcf03dd4a6997ff4b994ceb80e", $url, $message );


}

function test_should_not_add_token_if_signed_is_false() {
$message = "should not add token if signed is null";
$options = array( "type" => "authenticated", "version" => "1486020273" );
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg", $url, $message );


}

function test_null_token() {
$message = "should not add token if authToken is globally set but null auth token is explicitly set and signed = true";
$options = array(
"auth_token" => FALSE,
"sign_url" => TRUE,
"type" => "authenticated",
"version" => "1486020273"
);
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/authenticated/s--v2fTPYTu--/v1486020273/sample.jpg", $url, $message );


}

function test_explicit_authToken_should_override_global_setting() {
$message = "explicit authToken should override global setting";
$options = array(
"sign_url" => TRUE,
"auth_token" => array(
"key" => AuthTokenTest::ALT_KEY,
"start_time" => 222222222,
"duration" => 100
),
"type" => "authenticated",
"transformation" => array(
"crop" => "scale",
"width" => 300
)
);
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=7d276841d70c4ecbd0708275cd6a82e1f08e47838fbb0bceb2538e06ddfa3029", $url, $message );


}

function test_should_compute_expiration_as_start_time_plus_duration() {
$message = "should compute expiration as start time + duration";
$token = array( "key" => AuthTokenTest::KEY, "start_time" => 11111111, "duration" => 300 );
$options = array(
"sign_url" => TRUE,
"auth_token" => $token,
"resource_type" => "image",
"type" => "authenticated",
"version" => "1486020273"
);
$url = \Cloudinary::cloudinary_url( "sample.jpg", $options );
$this->assertEquals( "http://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3", $url, $message );

}

/**
* @expectedException \Cloudinary\Error
*/
function test_must_provide_expiration_or_duration() {

$message = "should throw if expiration and duration are not provided";
$token = array( "key" => AuthTokenTest::KEY, "expiration" => null, "duration" => null );
AuthToken::generate( $token );
$this->fail($message);
}



}

0 comments on commit ded9dc1

Please sign in to comment.