Matthias Andreas Benkard | b382b10 | 2021-01-02 15:32:21 +0100 | [diff] [blame^] | 1 | <?php |
| 2 | |
| 3 | namespace OAuth2\ResponseType; |
| 4 | |
| 5 | use OAuth2\Encryption\EncryptionInterface; |
| 6 | use OAuth2\Encryption\Jwt; |
| 7 | use OAuth2\Storage\AccessTokenInterface as AccessTokenStorageInterface; |
| 8 | use OAuth2\Storage\RefreshTokenInterface; |
| 9 | use OAuth2\Storage\PublicKeyInterface; |
| 10 | use OAuth2\Storage\Memory; |
| 11 | |
| 12 | /** |
| 13 | * @author Brent Shaffer <bshafs at gmail dot com> |
| 14 | */ |
| 15 | class JwtAccessToken extends AccessToken |
| 16 | { |
| 17 | protected $publicKeyStorage; |
| 18 | protected $encryptionUtil; |
| 19 | |
| 20 | /** |
| 21 | * @param PublicKeyInterface $publicKeyStorage - |
| 22 | * @param AccessTokenStorageInterface $tokenStorage - |
| 23 | * @param RefreshTokenInterface $refreshStorage - |
| 24 | * @param array $config - array with key store_encrypted_token_string (bool true) |
| 25 | * whether the entire encrypted string is stored, |
| 26 | * or just the token ID is stored |
| 27 | * @param EncryptionInterface $encryptionUtil - |
| 28 | */ |
| 29 | public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) |
| 30 | { |
| 31 | $this->publicKeyStorage = $publicKeyStorage; |
| 32 | $config = array_merge(array( |
| 33 | 'store_encrypted_token_string' => true, |
| 34 | 'issuer' => '' |
| 35 | ), $config); |
| 36 | if (is_null($tokenStorage)) { |
| 37 | // a pass-thru, so we can call the parent constructor |
| 38 | $tokenStorage = new Memory(); |
| 39 | } |
| 40 | if (is_null($encryptionUtil)) { |
| 41 | $encryptionUtil = new Jwt(); |
| 42 | } |
| 43 | $this->encryptionUtil = $encryptionUtil; |
| 44 | parent::__construct($tokenStorage, $refreshStorage, $config); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Handle the creation of access token, also issue refresh token if supported / desirable. |
| 49 | * |
| 50 | * @param mixed $client_id - Client identifier related to the access token. |
| 51 | * @param mixed $user_id - User ID associated with the access token |
| 52 | * @param string $scope - (optional) Scopes to be stored in space-separated string. |
| 53 | * @param bool $includeRefreshToken - If true, a new refresh_token will be added to the response |
| 54 | * @return array - The access token |
| 55 | * |
| 56 | * @see http://tools.ietf.org/html/rfc6749#section-5 |
| 57 | * @ingroup oauth2_section_5 |
| 58 | */ |
| 59 | public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) |
| 60 | { |
| 61 | // payload to encrypt |
| 62 | $payload = $this->createPayload($client_id, $user_id, $scope); |
| 63 | |
| 64 | /* |
| 65 | * Encode the payload data into a single JWT access_token string |
| 66 | */ |
| 67 | $access_token = $this->encodeToken($payload, $client_id); |
| 68 | |
| 69 | /* |
| 70 | * Save the token to a secondary storage. This is implemented on the |
| 71 | * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, |
| 72 | * if no secondary storage has been supplied |
| 73 | */ |
| 74 | $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $payload['id']; |
| 75 | $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); |
| 76 | |
| 77 | // token to return to the client |
| 78 | $token = array( |
| 79 | 'access_token' => $access_token, |
| 80 | 'expires_in' => $this->config['access_lifetime'], |
| 81 | 'token_type' => $this->config['token_type'], |
| 82 | 'scope' => $scope |
| 83 | ); |
| 84 | |
| 85 | /* |
| 86 | * Issue a refresh token also, if we support them |
| 87 | * |
| 88 | * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface |
| 89 | * is supplied in the constructor |
| 90 | */ |
| 91 | if ($includeRefreshToken && $this->refreshStorage) { |
| 92 | $refresh_token = $this->generateRefreshToken(); |
| 93 | $expires = 0; |
| 94 | if ($this->config['refresh_token_lifetime'] > 0) { |
| 95 | $expires = time() + $this->config['refresh_token_lifetime']; |
| 96 | } |
| 97 | $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope); |
| 98 | $token['refresh_token'] = $refresh_token; |
| 99 | } |
| 100 | |
| 101 | return $token; |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * @param array $token |
| 106 | * @param mixed $client_id |
| 107 | * @return mixed |
| 108 | */ |
| 109 | protected function encodeToken(array $token, $client_id = null) |
| 110 | { |
| 111 | $private_key = $this->publicKeyStorage->getPrivateKey($client_id); |
| 112 | $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); |
| 113 | |
| 114 | return $this->encryptionUtil->encode($token, $private_key, $algorithm); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * This function can be used to create custom JWT payloads |
| 119 | * |
| 120 | * @param mixed $client_id - Client identifier related to the access token. |
| 121 | * @param mixed $user_id - User ID associated with the access token |
| 122 | * @param string $scope - (optional) Scopes to be stored in space-separated string. |
| 123 | * @return array - The access token |
| 124 | */ |
| 125 | protected function createPayload($client_id, $user_id, $scope = null) |
| 126 | { |
| 127 | // token to encrypt |
| 128 | $expires = time() + $this->config['access_lifetime']; |
| 129 | $id = $this->generateAccessToken(); |
| 130 | |
| 131 | $payload = array( |
| 132 | 'id' => $id, // for BC (see #591) |
| 133 | 'jti' => $id, |
| 134 | 'iss' => $this->config['issuer'], |
| 135 | 'aud' => $client_id, |
| 136 | 'sub' => $user_id, |
| 137 | 'exp' => $expires, |
| 138 | 'iat' => time(), |
| 139 | 'token_type' => $this->config['token_type'], |
| 140 | 'scope' => $scope |
| 141 | ); |
| 142 | |
| 143 | if (isset($this->config['jwt_extra_payload_callable'])) { |
| 144 | if (!is_callable($this->config['jwt_extra_payload_callable'])) { |
| 145 | throw new \InvalidArgumentException('jwt_extra_payload_callable is not callable'); |
| 146 | } |
| 147 | |
| 148 | $extra = call_user_func($this->config['jwt_extra_payload_callable'], $client_id, $user_id, $scope); |
| 149 | |
| 150 | if (!is_array($extra)) { |
| 151 | throw new \InvalidArgumentException('jwt_extra_payload_callable must return array'); |
| 152 | } |
| 153 | |
| 154 | $payload = array_merge($extra, $payload); |
| 155 | } |
| 156 | |
| 157 | return $payload; |
| 158 | } |
| 159 | } |