1 : <?php
2 :
3 : /**
4 : * Twilio Capability Token generator
5 : *
6 : * @category Services
7 : * @package Services_Twilio
8 : * @author Jeff Lindsay <jeff.lindsay@twilio.com>
9 : * @license http://creativecommons.org/licenses/MIT/ MIT
10 : */
11 : class Services_Twilio_Capability
12 : {
13 : public $accountSid;
14 : public $authToken;
15 : public $scopes;
16 :
17 : /**
18 : * Create a new TwilioCapability with zero permissions. Next steps are to
19 : * grant access to resources by configuring this token through the
20 : * functions allowXXXX.
21 : *
22 : * @param $accountSid the account sid to which this token is granted access
23 : * @param $authToken the secret key used to sign the token. Note, this auth
24 : * token is not visible to the user of the token.
25 : */
26 : public function __construct($accountSid, $authToken)
27 : {
28 9 : $this->accountSid = $accountSid;
29 9 : $this->authToken = $authToken;
30 9 : $this->scopes = array();
31 9 : $this->clientName = false;
32 9 : }
33 :
34 : /**
35 : * If the user of this token should be allowed to accept incoming
36 : * connections then configure the TwilioCapability through this method and
37 : * specify the client name.
38 : *
39 : * @param $clientName
40 : */
41 : public function allowClientIncoming($clientName)
42 : {
43 :
44 : // clientName must be a non-zero length alphanumeric string
45 3 : if (preg_match('/\W/', $clientName)) {
46 1 : throw new InvalidArgumentException(
47 1 : 'Only alphanumeric characters allowed in client name.');
48 : }
49 :
50 2 : if (strlen($clientName) == 0) {
51 0 : throw new InvalidArgumentException(
52 0 : 'Client name must not be a zero length string.');
53 : }
54 :
55 2 : $this->clientName = $clientName;
56 2 : $this->allow('client', 'incoming',
57 2 : array('clientName' => $clientName));
58 2 : }
59 :
60 : /**
61 : * Allow the user of this token to make outgoing connections.
62 : *
63 : * @param $appSid the application to which this token grants access
64 : * @param $appParams signed parameters that the user of this token cannot
65 : * overwrite.
66 : */
67 : public function allowClientOutgoing($appSid, array $appParams=array())
68 : {
69 3 : $this->allow('client', 'outgoing', array(
70 3 : 'appSid' => $appSid,
71 3 : 'appParams' => http_build_query($appParams)));
72 3 : }
73 :
74 : /**
75 : * Allow the user of this token to access their event stream.
76 : *
77 : * @param $filters key/value filters to apply to the event stream
78 : */
79 : public function allowEventStream(array $filters=array())
80 : {
81 3 : $this->allow('stream', 'subscribe', array(
82 3 : 'path' => '/2010-04-01/Events',
83 3 : 'params' => http_build_query($filters),
84 3 : ));
85 3 : }
86 :
87 : /**
88 : * Generates a new token based on the credentials and permissions that
89 : * previously has been granted to this token.
90 : *
91 : * @param $ttl the expiration time of the token (in seconds). Default
92 : * value is 3600 (1hr)
93 : * @return the newly generated token that is valid for $ttl seconds
94 : */
95 : public function generateToken($ttl = 3600)
96 : {
97 : $payload = array(
98 8 : 'scope' => array(),
99 8 : 'iss' => $this->accountSid,
100 8 : 'exp' => time() + $ttl,
101 8 : );
102 8 : $scopeStrings = array();
103 :
104 8 : foreach ($this->scopes as $scope) {
105 6 : if ($scope->privilege == "outgoing" && $this->clientName)
106 6 : $scope->params["clientName"] = $this->clientName;
107 6 : $scopeStrings[] = $scope->toString();
108 8 : }
109 :
110 8 : $payload['scope'] = implode(' ', $scopeStrings);
111 8 : return JWT::encode($payload, $this->authToken, 'HS256');
112 : }
113 :
114 : protected function allow($service, $privilege, $params) {
115 6 : $this->scopes[] = new ScopeURI($service, $privilege, $params);
116 6 : }
117 : }
118 :
119 : /**
120 : * Scope URI implementation
121 : *
122 : * Simple way to represent configurable privileges in an OAuth
123 : * friendly way. For our case, they look like this:
124 : *
125 : * scope:<service>:<privilege>?<params>
126 : *
127 : * For example:
128 : * scope:client:incoming?name=jonas
129 : *
130 : * @author Jeff Lindsay <jeff.lindsay@twilio.com>
131 : */
132 : class ScopeURI
133 : {
134 : public $service;
135 : public $privilege;
136 : public $params;
137 :
138 : public function __construct($service, $privilege, $params = array())
139 : {
140 6 : $this->service = $service;
141 6 : $this->privilege = $privilege;
142 6 : $this->params = $params;
143 6 : }
144 :
145 : public function toString()
146 : {
147 6 : $uri = "scope:{$this->service}:{$this->privilege}";
148 6 : if (count($this->params)) {
149 6 : $uri .= "?".http_build_query($this->params);
150 6 : }
151 6 : return $uri;
152 : }
153 :
154 : /**
155 : * Parse a scope URI into a ScopeURI object
156 : *
157 : * @param string $uri The scope URI
158 : * @return ScopeURI The parsed scope uri
159 : */
160 : public static function parse($uri)
161 : {
162 0 : if (strpos($uri, 'scope:') !== 0) {
163 0 : throw new UnexpectedValueException(
164 0 : 'Not a scope URI according to scheme');
165 : }
166 :
167 0 : $parts = explode('?', $uri, 1);
168 0 : $params = null;
169 :
170 0 : if (count($parts) > 1) {
171 0 : parse_str($parts[1], $params);
172 0 : }
173 :
174 0 : $parts = explode(':', $parts[0], 2);
175 :
176 0 : if (count($parts) != 3) {
177 0 : throw new UnexpectedValueException(
178 0 : 'Not enough parts for scope URI');
179 : }
180 :
181 0 : list($scheme, $service, $privilege) = $parts;
182 0 : return new ScopeURI($service, $privilege, $params);
183 : }
184 :
185 : }
186 :
187 : /**
188 : * JSON Web Token implementation
189 : *
190 : * Minimum implementation used by Realtime auth, based on this spec:
191 : * http://self-issued.info/docs/draft-jones-json-web-token-01.html.
192 : *
193 : * @author Neuman Vong <neuman@twilio.com>
194 : */
195 : class JWT
196 : {
197 : /**
198 : * @param string $jwt The JWT
199 : * @param string|null $key The secret key
200 : * @param bool $verify Don't skip verification process
201 : *
202 : * @return object The JWT's payload as a PHP object
203 : */
204 : public static function decode($jwt, $key = null, $verify = true)
205 : {
206 8 : $tks = explode('.', $jwt);
207 8 : if (count($tks) != 3) {
208 0 : throw new UnexpectedValueException('Wrong number of segments');
209 : }
210 8 : list($headb64, $payloadb64, $cryptob64) = $tks;
211 8 : if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))
212 8 : ) {
213 0 : throw new UnexpectedValueException('Invalid segment encoding');
214 : }
215 8 : if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))
216 8 : ) {
217 0 : throw new UnexpectedValueException('Invalid segment encoding');
218 : }
219 8 : $sig = JWT::urlsafeB64Decode($cryptob64);
220 8 : if ($verify) {
221 8 : if (empty($header->alg)) {
222 0 : throw new DomainException('Empty algorithm');
223 : }
224 8 : if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) {
225 0 : throw new UnexpectedValueException('Signature verification failed');
226 : }
227 8 : }
228 8 : return $payload;
229 : }
230 :
231 : /**
232 : * @param object|array $payload PHP object or array
233 : * @param string $key The secret key
234 : * @param string $algo The signing algorithm
235 : *
236 : * @return string A JWT
237 : */
238 : public static function encode($payload, $key, $algo = 'HS256')
239 : {
240 8 : $header = array('typ' => 'JWT', 'alg' => $algo);
241 :
242 8 : $segments = array();
243 8 : $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
244 8 : $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
245 8 : $signing_input = implode('.', $segments);
246 :
247 8 : $signature = JWT::sign($signing_input, $key, $algo);
248 8 : $segments[] = JWT::urlsafeB64Encode($signature);
249 :
250 8 : return implode('.', $segments);
251 : }
252 :
253 : /**
254 : * @param string $msg The message to sign
255 : * @param string $key The secret key
256 : * @param string $method The signing algorithm
257 : *
258 : * @return string An encrypted message
259 : */
260 : public static function sign($msg, $key, $method = 'HS256')
261 : {
262 : $methods = array(
263 8 : 'HS256' => 'sha256',
264 8 : 'HS384' => 'sha384',
265 8 : 'HS512' => 'sha512',
266 8 : );
267 8 : if (empty($methods[$method])) {
268 0 : throw new DomainException('Algorithm not supported');
269 : }
270 8 : return hash_hmac($methods[$method], $msg, $key, true);
271 : }
272 :
273 : /**
274 : * @param string $input JSON string
275 : *
276 : * @return object Object representation of JSON string
277 : */
278 : public static function jsonDecode($input)
279 : {
280 8 : $obj = json_decode($input);
281 8 : if (function_exists('json_last_error') && $errno = json_last_error()) {
282 0 : JWT::handleJsonError($errno);
283 0 : }
284 8 : else if ($obj === null && $input !== 'null') {
285 0 : throw new DomainException('Null result with non-null input');
286 : }
287 8 : return $obj;
288 : }
289 :
290 : /**
291 : * @param object|array $input A PHP object or array
292 : *
293 : * @return string JSON representation of the PHP object or array
294 : */
295 : public static function jsonEncode($input)
296 : {
297 8 : $json = json_encode($input);
298 8 : if (function_exists('json_last_error') && $errno = json_last_error()) {
299 0 : JWT::handleJsonError($errno);
300 0 : }
301 8 : else if ($json === 'null' && $input !== null) {
302 0 : throw new DomainException('Null result with non-null input');
303 : }
304 8 : return $json;
305 : }
306 :
307 : /**
308 : * @param string $input A base64 encoded string
309 : *
310 : * @return string A decoded string
311 : */
312 : public static function urlsafeB64Decode($input)
313 : {
314 8 : $padlen = 4 - strlen($input) % 4;
315 8 : $input .= str_repeat('=', $padlen);
316 8 : return base64_decode(strtr($input, '-_', '+/'));
317 : }
318 :
319 : /**
320 : * @param string $input Anything really
321 : *
322 : * @return string The base64 encode of what you passed in
323 : */
324 : public static function urlsafeB64Encode($input)
325 : {
326 8 : return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
327 : }
328 :
329 : /**
330 : * @param int $errno An error number from json_last_error()
331 : *
332 : * @return void
333 : */
334 : private static function handleJsonError($errno)
335 : {
336 : $messages = array(
337 0 : JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
338 0 : JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
339 : JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
340 0 : );
341 0 : throw new DomainException(isset($messages[$errno])
342 0 : ? $messages[$errno]
343 0 : : 'Unknown JSON error: ' . $errno
344 0 : );
345 : }
|