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