Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
namespace Aws\Token;
3
 
4
use Aws\Exception\TokenException;
1441 ariadna 5
use Aws\SSOOIDC\SSOOIDCClient;
1 efrain 6
use GuzzleHttp\Promise;
7
 
8
/**
9
 * Token that comes from the SSO provider
10
 */
11
class SsoTokenProvider implements RefreshableTokenProviderInterface
12
{
13
    use ParsesIniTrait;
14
 
15
    const ENV_PROFILE = 'AWS_PROFILE';
1441 ariadna 16
    const REFRESH_WINDOW_IN_SECS = 300;
17
    const REFRESH_ATTEMPT_WINDOW_IN_SECS = 30;
1 efrain 18
 
1441 ariadna 19
    /** @var string $profileName */
20
    private $profileName;
21
 
22
    /** @var string $configFilePath */
23
    private $configFilePath;
24
 
25
    /** @var SSOOIDCClient $ssoOidcClient */
1 efrain 26
    private $ssoOidcClient;
27
 
1441 ariadna 28
    /** @var string $ssoSessionName */
29
    private $ssoSessionName;
30
 
1 efrain 31
    /**
32
     * Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile
1441 ariadna 33
     * @param string $profileName The name of the profile that contains the sso_session key
34
     * @param string|null $configFilePath Name of the config file to sso profile from
35
     * @param SSOOIDCClient|null $ssoOidcClient The sso client for generating a new token
1 efrain 36
     */
1441 ariadna 37
    public function __construct(
38
        $profileName,
39
        $configFilePath = null,
40
        ?SSOOIDCClient $ssoOidcClient = null
41
    ) {
42
        $this->profileName = $this->resolveProfileName($profileName);
43
        $this->configFilePath =  $this->resolveConfigFile($configFilePath);
1 efrain 44
        $this->ssoOidcClient = $ssoOidcClient;
45
    }
46
 
1441 ariadna 47
    /**
48
     * This method resolves the profile name to be used. The
49
     * profile provided as instantiation argument takes precedence,
50
     * followed by AWS_PROFILE env variable, otherwise `default` is
51
     * used.
1 efrain 52
     *
1441 ariadna 53
     * @param string|null $argProfileName The profile provided as argument.
54
     *
55
     * @return string
1 efrain 56
     */
1441 ariadna 57
    private function resolveProfileName($argProfileName): string
58
    {
59
        if (empty($argProfileName)) {
60
            return getenv(self::ENV_PROFILE) ?: 'default';
61
        } else {
62
            return $argProfileName;
63
        }
64
    }
65
 
66
    /**
67
     * This method resolves the config file from where the profiles
68
     * are going to be loaded from. If $argFileName is not empty then,
69
     * it takes precedence over the default config file location.
70
     *
71
     * @param string|null $argConfigFilePath The config path provided as argument.
72
     *
73
     * @return string
74
     */
75
    private function resolveConfigFile($argConfigFilePath): string
76
    {
77
        if (empty($argConfigFilePath)) {
78
            return self::getHomeDir() . '/.aws/config';
79
        } else{
80
            return $argConfigFilePath;
81
        }
82
    }
83
 
84
    /**
85
     *  Loads cached sso credentials.
86
     *
87
     * @return Promise\PromiseInterface
88
     */
1 efrain 89
    public function __invoke()
90
    {
91
        return Promise\Coroutine::of(function () {
1441 ariadna 92
            if (empty($this->configFilePath) || !is_readable($this->configFilePath)) {
93
                throw new TokenException("Cannot read profiles from {$this->configFilePath}");
1 efrain 94
            }
1441 ariadna 95
 
96
            $profiles = self::loadProfiles($this->configFilePath);
97
            if (!isset($profiles[$this->profileName])) {
98
                throw new TokenException("Profile `{$this->profileName}` does not exist in {$this->configFilePath}.");
1 efrain 99
            }
1441 ariadna 100
 
101
            $profile = $profiles[$this->profileName];
102
            if (empty($profile['sso_session'])) {
1 efrain 103
                throw new TokenException(
1441 ariadna 104
                    "Profile `{$this->profileName}` in {$this->configFilePath} must contain an sso_session."
1 efrain 105
                );
106
            }
107
 
1441 ariadna 108
            $ssoSessionName = $profile['sso_session'];
109
            $this->ssoSessionName = $ssoSessionName;
110
            $profileSsoSession = 'sso-session ' . $ssoSessionName;
111
            if (empty($profiles[$profileSsoSession])) {
1 efrain 112
                throw new TokenException(
1441 ariadna 113
                    "Sso session `{$ssoSessionName}` does not exist in {$this->configFilePath}"
1 efrain 114
                );
115
            }
116
 
1441 ariadna 117
            $sessionProfileData = $profiles[$profileSsoSession];
118
            foreach (['sso_start_url', 'sso_region'] as $requiredProp) {
119
                if (empty($sessionProfileData[$requiredProp])) {
120
                    throw new TokenException(
121
                        "Sso session `{$ssoSessionName}` in {$this->configFilePath} is missing the required property `{$requiredProp}`"
122
                    );
123
                }
1 efrain 124
            }
125
 
1441 ariadna 126
            $tokenData = $this->refresh();
127
            $tokenLocation = self::getTokenLocation($ssoSessionName);
128
            $this->validateTokenData($tokenLocation, $tokenData);
129
            $ssoToken = SsoToken::fromTokenData($tokenData);
130
            // To make sure the token is not expired
131
            if ($ssoToken->isExpired()) {
132
                throw new TokenException("Cached SSO token returned an expired token.");
1 efrain 133
            }
1441 ariadna 134
 
135
            yield $ssoToken;
1 efrain 136
        });
137
    }
138
 
139
    /**
1441 ariadna 140
     * This method attempt to refresh when possible.
141
     * If a refresh is not possible then it just returns
142
     * the current token data as it is.
143
     *
144
     * @return array
145
     * @throws TokenException
1 efrain 146
     */
1441 ariadna 147
    public function refresh(): array
148
    {
149
        $tokenLocation = self::getTokenLocation($this->ssoSessionName);
150
        $tokenData = $this->getTokenData($tokenLocation);
151
        if (!$this->shouldAttemptRefresh()) {
152
            return $tokenData;
153
        }
154
 
155
        if (null === $this->ssoOidcClient) {
156
            throw new TokenException(
157
                "Cannot refresh this token without an 'ssooidcClient' "
158
            );
159
        }
160
 
161
        foreach (['clientId', 'clientSecret', 'refreshToken'] as $requiredProp) {
162
            if (empty($tokenData[$requiredProp])) {
1 efrain 163
                throw new TokenException(
1441 ariadna 164
                    "Cannot refresh this token without `{$requiredProp}` being set"
1 efrain 165
                );
166
            }
1441 ariadna 167
        }
1 efrain 168
 
1441 ariadna 169
        $response = $this->ssoOidcClient->createToken([
170
            'clientId' => $tokenData['clientId'],
171
            'clientSecret' => $tokenData['clientSecret'],
172
            'grantType' => 'refresh_token', // REQUIRED
173
            'refreshToken' => $tokenData['refreshToken'],
174
        ]);
175
        if ($response['@metadata']['statusCode'] !== 200) {
176
            throw new TokenException('Unable to create a new sso token');
177
        }
1 efrain 178
 
1441 ariadna 179
        $tokenData['accessToken'] = $response['accessToken'];
180
        $tokenData['expiresAt'] = time () + $response['expiresIn'];
181
        $tokenData['refreshToken'] = $response['refreshToken'];
182
 
183
        return $this->writeNewTokenDataToDisk($tokenData, $tokenLocation);
1 efrain 184
    }
185
 
1441 ariadna 186
    /**
187
     * This method checks for whether a token refresh should happen.
188
     * It will return true just if more than 30 seconds has happened
189
     * since last refresh, and if the expiration is within a 5-minutes
190
     * window from the current time.
191
     *
192
     * @return bool
193
     */
194
    public function shouldAttemptRefresh(): bool
1 efrain 195
    {
1441 ariadna 196
        $tokenLocation = self::getTokenLocation($this->ssoSessionName);
1 efrain 197
        $tokenData = $this->getTokenData($tokenLocation);
1441 ariadna 198
        if (empty($tokenData['expiresAt'])) {
199
            throw new TokenException(
200
                "Token file at $tokenLocation must contain an expiration date"
201
            );
202
        }
203
 
204
        $tokenExpiresAt = strtotime($tokenData['expiresAt']);
205
        $lastRefreshAt = filemtime($tokenLocation);
206
        $now = \time();
207
 
208
        // If last refresh happened after 30 seconds
209
        // and if the token expiration is in the 5 minutes window
210
        return ($now - $lastRefreshAt) > self::REFRESH_ATTEMPT_WINDOW_IN_SECS
211
            && ($tokenExpiresAt - $now) < self::REFRESH_WINDOW_IN_SECS;
1 efrain 212
    }
213
 
214
    /**
215
     * @param $sso_session
216
     * @return string
217
     */
1441 ariadna 218
    public static function getTokenLocation($sso_session): string
1 efrain 219
    {
220
        return self::getHomeDir()
221
            . '/.aws/sso/cache/'
222
            . mb_convert_encoding(sha1($sso_session), "UTF-8")
223
            . ".json";
224
    }
225
 
226
    /**
227
     * @param $tokenLocation
228
     * @return array
229
     */
1441 ariadna 230
    function getTokenData($tokenLocation): array
1 efrain 231
    {
1441 ariadna 232
        if (empty($tokenLocation) || !is_readable($tokenLocation)) {
233
            throw new TokenException("Unable to read token file at {$tokenLocation}");
234
        }
235
 
1 efrain 236
        return json_decode(file_get_contents($tokenLocation), true);
237
    }
238
 
239
    /**
240
     * @param $tokenData
241
     * @param $tokenLocation
242
     * @return mixed
243
     */
244
    private function validateTokenData($tokenLocation, $tokenData)
245
    {
1441 ariadna 246
        foreach (['accessToken', 'expiresAt'] as $requiredProp) {
247
            if (empty($tokenData[$requiredProp])) {
248
                throw new TokenException(
249
                    "Token file at {$tokenLocation} must contain the required property `{$requiredProp}`"
250
                );
251
            }
1 efrain 252
        }
253
 
254
        $expiration = strtotime($tokenData['expiresAt']);
255
        if ($expiration === false) {
256
            throw new TokenException("Cached SSO token returned an invalid expiration");
257
        } elseif ($expiration < time()) {
258
            throw new TokenException("Cached SSO token returned an expired token");
259
        }
1441 ariadna 260
 
1 efrain 261
        return $tokenData;
262
    }
263
 
264
    /**
265
     * @param array $tokenData
266
     * @param string $tokenLocation
1441 ariadna 267
     *
268
     * @return array
1 efrain 269
     */
1441 ariadna 270
    private function writeNewTokenDataToDisk(array $tokenData, $tokenLocation): array
1 efrain 271
    {
272
        $tokenData['expiresAt'] = gmdate(
273
            'Y-m-d\TH:i:s\Z',
274
            $tokenData['expiresAt']
275
        );
276
        file_put_contents($tokenLocation, json_encode(array_filter($tokenData)));
1441 ariadna 277
 
278
        return $tokenData;
1 efrain 279
    }
280
}