| 
 | 1 | +<?php  | 
 | 2 | + | 
 | 3 | +declare(strict_types=1);  | 
 | 4 | + | 
 | 5 | +namespace AsyncAws\Core\Credentials;  | 
 | 6 | + | 
 | 7 | +use AsyncAws\Core\EnvVar;  | 
 | 8 | +use AsyncAws\SsoOidc\SsoOidcClient;  | 
 | 9 | +use Psr\Log\LoggerInterface;  | 
 | 10 | +use Psr\Log\NullLogger;  | 
 | 11 | +use Symfony\Contracts\HttpClient\HttpClientInterface;  | 
 | 12 | + | 
 | 13 | +/**  | 
 | 14 | + * Load and refresh AWS SSO tokens.  | 
 | 15 | + *  | 
 | 16 | + * @internal  | 
 | 17 | + */  | 
 | 18 | +final class SsoTokenProvider  | 
 | 19 | +{  | 
 | 20 | +    public const KEY_CLIENT_ID = 'clientId';  | 
 | 21 | +    public const KEY_CLIENT_SECRET = 'clientSecret';  | 
 | 22 | +    public const KEY_REFRESH_TOKEN = 'refreshToken';  | 
 | 23 | +    public const KEY_ACCESS_TOKEN = 'accessToken';  | 
 | 24 | +    public const KEY_EXPIRES_AT = 'expiresAt';  | 
 | 25 | + | 
 | 26 | +    private const REFRESH_WINDOW = 300;  | 
 | 27 | + | 
 | 28 | +    /**  | 
 | 29 | +     * @var LoggerInterface  | 
 | 30 | +     */  | 
 | 31 | +    private $logger;  | 
 | 32 | + | 
 | 33 | +    /**  | 
 | 34 | +     * @var ?HttpClientInterface  | 
 | 35 | +     */  | 
 | 36 | +    private $httpClient;  | 
 | 37 | + | 
 | 38 | +    public function __construct(?HttpClientInterface $httpClient = null, ?LoggerInterface $logger = null)  | 
 | 39 | +    {  | 
 | 40 | +        $this->httpClient = $httpClient;  | 
 | 41 | +        $this->logger = $logger ?? new NullLogger();  | 
 | 42 | +    }  | 
 | 43 | + | 
 | 44 | +    /**  | 
 | 45 | +     * @param array<string, string> $sessionData  | 
 | 46 | +     */  | 
 | 47 | +    public function getToken(string $sessionName, array $sessionData): ?string  | 
 | 48 | +    {  | 
 | 49 | +        $tokenData = $this->loadSsoToken($sessionName);  | 
 | 50 | +        if (null === $tokenData) {  | 
 | 51 | +            return null;  | 
 | 52 | +        }  | 
 | 53 | + | 
 | 54 | +        $tokenData = $this->refreshTokenIfNeeded($sessionName, $sessionData, $tokenData);  | 
 | 55 | +        if (!isset($tokenData[self::KEY_ACCESS_TOKEN])) {  | 
 | 56 | +            $this->logger->warning('The token for SSO session "{session}" does not contains accessToken.', ['session' => $sessionName]);  | 
 | 57 | + | 
 | 58 | +            return null;  | 
 | 59 | +        }  | 
 | 60 | + | 
 | 61 | +        return $tokenData[self::KEY_ACCESS_TOKEN];  | 
 | 62 | +    }  | 
 | 63 | + | 
 | 64 | +    /**  | 
 | 65 | +     * @param array<string, string> $sessionData  | 
 | 66 | +     */  | 
 | 67 | +    private function refreshTokenIfNeeded(string $sessionName, array $sessionData, array $tokenData): array  | 
 | 68 | +    {  | 
 | 69 | +        if (!isset($tokenData[self::KEY_EXPIRES_AT])) {  | 
 | 70 | +            $this->logger->warning('The token for SSO session "{session}" does not contains expiration date.', ['session' => $sessionName]);  | 
 | 71 | + | 
 | 72 | +            return $tokenData;  | 
 | 73 | +        }  | 
 | 74 | + | 
 | 75 | +        $tokenExpiresAt = new \DateTimeImmutable($tokenData[self::KEY_EXPIRES_AT]);  | 
 | 76 | +        $tokenRefreshAt = $tokenExpiresAt->sub(new \DateInterval(\sprintf('PT%dS', self::REFRESH_WINDOW)));  | 
 | 77 | + | 
 | 78 | +        // If token expiration is in the 5 minutes window  | 
 | 79 | +        if ($tokenRefreshAt > new \DateTimeImmutable()) {  | 
 | 80 | +            return $tokenData;  | 
 | 81 | +        }  | 
 | 82 | + | 
 | 83 | +        if (!isset(  | 
 | 84 | +            $tokenData[self::KEY_CLIENT_ID],  | 
 | 85 | +            $tokenData[self::KEY_CLIENT_SECRET],  | 
 | 86 | +            $tokenData[self::KEY_REFRESH_TOKEN]  | 
 | 87 | +        )) {  | 
 | 88 | +            $this->logger->warning('The token for SSO session "{session}" does not contains required properties and cannot be refreshed.', ['session' => $sessionName]);  | 
 | 89 | + | 
 | 90 | +            return $tokenData;  | 
 | 91 | +        }  | 
 | 92 | + | 
 | 93 | +        $ssoOidcClient = new SsoOidcClient(  | 
 | 94 | +            ['region' => $sessionData[IniFileLoader::KEY_SSO_REGION]],  | 
 | 95 | +            new NullProvider(),  | 
 | 96 | +            // no credentials required as we provide an access token via the role credentials request  | 
 | 97 | +            $this->httpClient  | 
 | 98 | +        );  | 
 | 99 | + | 
 | 100 | +        $result = $ssoOidcClient->createToken([  | 
 | 101 | +            'clientId' => $tokenData[self::KEY_CLIENT_ID],  | 
 | 102 | +            'clientSecret' => $tokenData[self::KEY_CLIENT_SECRET],  | 
 | 103 | +            'grantType' => 'refresh_token', // REQUIRED  | 
 | 104 | +            'refreshToken' => $tokenData[self::KEY_REFRESH_TOKEN],  | 
 | 105 | +        ]);  | 
 | 106 | + | 
 | 107 | +        $tokenData = [  | 
 | 108 | +            self::KEY_ACCESS_TOKEN => $result->getAccessToken(),  | 
 | 109 | +            self::KEY_REFRESH_TOKEN => $result->getRefreshToken(),  | 
 | 110 | +        ] + $tokenData;  | 
 | 111 | + | 
 | 112 | +        if (null === $expiresIn = $result->getExpiresIn()) {  | 
 | 113 | +            $this->logger->warning('The token for SSO session "{session}" does not contains expiration time.', ['session' => $sessionName]);  | 
 | 114 | +        } else {  | 
 | 115 | +            $tokenData[self::KEY_EXPIRES_AT] = (new \DateTimeImmutable())->add(new \DateInterval(\sprintf('PT%dS', $expiresIn)))->format(\DateTime::ATOM);  | 
 | 116 | +        }  | 
 | 117 | + | 
 | 118 | +        $this->dumpSsoToken($sessionName, $tokenData);  | 
 | 119 | + | 
 | 120 | +        return $tokenData;  | 
 | 121 | +    }  | 
 | 122 | + | 
 | 123 | +    private function dumpSsoToken(string $sessionName, array $tokenData): void  | 
 | 124 | +    {  | 
 | 125 | +        $filepath = \sprintf('%s/.aws/sso/cache/%s.json', $this->getHomeDir(), sha1($sessionName));  | 
 | 126 | + | 
 | 127 | +        file_put_contents($filepath, json_encode(array_filter($tokenData)));  | 
 | 128 | +    }  | 
 | 129 | + | 
 | 130 | +    /**  | 
 | 131 | +     * @return array<string, string>|null  | 
 | 132 | +     */  | 
 | 133 | +    private function loadSsoToken(string $sessionName): ?array  | 
 | 134 | +    {  | 
 | 135 | +        $filepath = \sprintf('%s/.aws/sso/cache/%s.json', $this->getHomeDir(), sha1($sessionName));  | 
 | 136 | +        if (!is_readable($filepath)) {  | 
 | 137 | +            $this->logger->warning('The sso cache file {path} is not readable.', ['path' => $filepath]);  | 
 | 138 | + | 
 | 139 | +            return null;  | 
 | 140 | +        }  | 
 | 141 | + | 
 | 142 | +        if (false === ($content = @file_get_contents($filepath))) {  | 
 | 143 | +            $this->logger->warning('The sso cache file {path} is not readable.', ['path' => $filepath]);  | 
 | 144 | + | 
 | 145 | +            return null;  | 
 | 146 | +        }  | 
 | 147 | + | 
 | 148 | +        try {  | 
 | 149 | +            return json_decode(  | 
 | 150 | +                $content,  | 
 | 151 | +                true,  | 
 | 152 | +                512,  | 
 | 153 | +                \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0)  | 
 | 154 | +            );  | 
 | 155 | +        } catch (\JsonException $e) {  | 
 | 156 | +            $this->logger->warning(  | 
 | 157 | +                'The sso cache file {path} contains invalide JSON.',  | 
 | 158 | +                ['path' => $filepath, 'ecxeption' => $e]  | 
 | 159 | +            );  | 
 | 160 | + | 
 | 161 | +            return null;  | 
 | 162 | +        }  | 
 | 163 | +    }  | 
 | 164 | + | 
 | 165 | +    private function getHomeDir(): string  | 
 | 166 | +    {  | 
 | 167 | +        // On Linux/Unix-like systems, use the HOME environment variable  | 
 | 168 | +        if (null !== $homeDir = EnvVar::get('HOME')) {  | 
 | 169 | +            return $homeDir;  | 
 | 170 | +        }  | 
 | 171 | + | 
 | 172 | +        // Get the HOMEDRIVE and HOMEPATH values for Windows hosts  | 
 | 173 | +        $homeDrive = EnvVar::get('HOMEDRIVE');  | 
 | 174 | +        $homePath = EnvVar::get('HOMEPATH');  | 
 | 175 | + | 
 | 176 | +        return ($homeDrive && $homePath) ? $homeDrive . $homePath : '/';  | 
 | 177 | +    }  | 
 | 178 | +}  | 
0 commit comments