Proyectos de Subversion Moodle

Rev

Rev 1 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

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