1<?php 2/* 3 * Copyright 2015 Google Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18namespace Google\Auth\Middleware; 19 20use Google\Auth\CacheTrait; 21use Psr\Cache\CacheItemPoolInterface; 22use Psr\Http\Message\RequestInterface; 23 24/** 25 * ScopedAccessTokenMiddleware is a Guzzle Middleware that adds an Authorization 26 * header provided by a closure. 27 * 28 * The closure returns an access token, taking the scope, either a single 29 * string or an array of strings, as its value. If provided, a cache will be 30 * used to preserve the access token for a given lifetime. 31 * 32 * Requests will be accessed with the authorization header: 33 * 34 * 'authorization' 'Bearer <value of auth_token>' 35 */ 36class ScopedAccessTokenMiddleware 37{ 38 use CacheTrait; 39 40 const DEFAULT_CACHE_LIFETIME = 1500; 41 42 /** 43 * @var callable 44 */ 45 private $tokenFunc; 46 47 /** 48 * @var array<string>|string 49 */ 50 private $scopes; 51 52 /** 53 * Creates a new ScopedAccessTokenMiddleware. 54 * 55 * @param callable $tokenFunc a token generator function 56 * @param array<string>|string $scopes the token authentication scopes 57 * @param array<mixed> $cacheConfig configuration for the cache when it's present 58 * @param CacheItemPoolInterface $cache an implementation of CacheItemPoolInterface 59 */ 60 public function __construct( 61 callable $tokenFunc, 62 $scopes, 63 array $cacheConfig = null, 64 CacheItemPoolInterface $cache = null 65 ) { 66 $this->tokenFunc = $tokenFunc; 67 if (!(is_string($scopes) || is_array($scopes))) { 68 throw new \InvalidArgumentException( 69 'wants scope should be string or array' 70 ); 71 } 72 $this->scopes = $scopes; 73 74 if (!is_null($cache)) { 75 $this->cache = $cache; 76 $this->cacheConfig = array_merge([ 77 'lifetime' => self::DEFAULT_CACHE_LIFETIME, 78 'prefix' => '', 79 ], $cacheConfig); 80 } 81 } 82 83 /** 84 * Updates the request with an Authorization header when auth is 'scoped'. 85 * 86 * E.g this could be used to authenticate using the AppEngine 87 * AppIdentityService. 88 * 89 * use google\appengine\api\app_identity\AppIdentityService; 90 * use Google\Auth\Middleware\ScopedAccessTokenMiddleware; 91 * use GuzzleHttp\Client; 92 * use GuzzleHttp\HandlerStack; 93 * 94 * $scope = 'https://www.googleapis.com/auth/taskqueue' 95 * $middleware = new ScopedAccessTokenMiddleware( 96 * 'AppIdentityService::getAccessToken', 97 * $scope, 98 * [ 'prefix' => 'Google\Auth\ScopedAccessToken::' ], 99 * $cache = new Memcache() 100 * ); 101 * $stack = HandlerStack::create(); 102 * $stack->push($middleware); 103 * 104 * $client = new Client([ 105 * 'handler' => $stack, 106 * 'base_url' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/', 107 * 'auth' => 'scoped' // authorize all requests 108 * ]); 109 * 110 * $res = $client->get('myproject/taskqueues/myqueue'); 111 * 112 * @param callable $handler 113 * @return \Closure 114 */ 115 public function __invoke(callable $handler) 116 { 117 return function (RequestInterface $request, array $options) use ($handler) { 118 // Requests using "auth"="scoped" will be authorized. 119 if (!isset($options['auth']) || $options['auth'] !== 'scoped') { 120 return $handler($request, $options); 121 } 122 123 $request = $request->withHeader('authorization', 'Bearer ' . $this->fetchToken()); 124 125 return $handler($request, $options); 126 }; 127 } 128 129 /** 130 * @return string 131 */ 132 private function getCacheKey() 133 { 134 $key = null; 135 136 if (is_string($this->scopes)) { 137 $key .= $this->scopes; 138 } elseif (is_array($this->scopes)) { 139 $key .= implode(':', $this->scopes); 140 } 141 142 return $key; 143 } 144 145 /** 146 * Determine if token is available in the cache, if not call tokenFunc to 147 * fetch it. 148 * 149 * @return string 150 */ 151 private function fetchToken() 152 { 153 $cacheKey = $this->getCacheKey(); 154 $cached = $this->getCachedValue($cacheKey); 155 156 if (!empty($cached)) { 157 return $cached; 158 } 159 160 $token = call_user_func($this->tokenFunc, $this->scopes); 161 $this->setCachedValue($cacheKey, $token); 162 163 return $token; 164 } 165} 166