| @@ -69,17 +69,25 @@ | ||
| 69 | 69 | */ |
| 70 | 70 | private $error_count; |
| 71 | 71 | |
| 72 | 72 | /** |
| 73 | + * The RFC 8707 resource indicator this token was minted for. | |
| 74 | + * | |
| 75 | + * @var Resource_Indicator | |
| 76 | + */ | |
| 77 | + private $resource_indicator; | |
| 78 | + | |
| 79 | + /** | |
| 73 | 80 | * Token_Set constructor. |
| 74 | 81 | * |
| 75 | - * @param string $access_token The access token. | |
| 76 | - * @param int $expires_at Unix timestamp of token expiry. | |
| 77 | - * @param string $token_type An Auth_Token_Type constant. | |
| 78 | - * @param string|null $refresh_token The refresh token. | |
| 79 | - * @param string|null $id_token The OIDC ID token. | |
| 80 | - * @param string|null $scope The granted scope. | |
| 81 | - * @param int $error_count The number of consecutive refresh errors. | |
| 82 | + * @param string $access_token The access token. | |
| 83 | + * @param int $expires_at Unix timestamp of token expiry. | |
| 84 | + * @param string $token_type An Auth_Token_Type constant. | |
| 85 | + * @param string|null $refresh_token The refresh token. | |
| 86 | + * @param string|null $id_token The OIDC ID token. | |
| 87 | + * @param string|null $scope The granted scope. | |
| 88 | + * @param int $error_count The number of consecutive refresh errors. | |
| 89 | + * @param Resource_Indicator|null $resource_indicator The resource indicator (RFC 8707) the token was minted for. Null is treated as Resource_Indicator::default(). | |
| 82 | 90 | * |
| 83 | 91 | * @throws InvalidArgumentException If required fields are empty or invalid. |
| 84 | 92 | */ |
| 85 | 93 | public function __construct( |
| @@ -92,9 +100,10 @@ | ||
| 92 | 100 | #[SensitiveParameter] |
| 93 | 101 | ?string $refresh_token = null, |
| 94 | 102 | ?string $id_token = null, |
| 95 | 103 | ?string $scope = null, |
| 96 | - int $error_count = 0 | |
| 104 | + int $error_count = 0, | |
| 105 | + ?Resource_Indicator $resource_indicator = null | |
| 97 | 106 | ) { |
| 98 | 107 | if ( $access_token === '' ) { |
| 99 | 108 | throw new InvalidArgumentException( 'Token_Set requires a non-empty access_token.' ); |
| 100 | 109 | } |
| @@ -104,15 +113,16 @@ | ||
| 104 | 113 | if ( $token_type === '' ) { |
| 105 | 114 | throw new InvalidArgumentException( 'Token_Set requires a non-empty token_type.' ); |
| 106 | 115 | } |
| 107 | 116 | |
| 108 | - $this->access_token = $access_token; | |
| 109 | - $this->expires_at = $expires_at; | |
| 110 | - $this->token_type = $token_type; | |
| 111 | - $this->refresh_token = $refresh_token; | |
| 112 | - $this->id_token = $id_token; | |
| 113 | - $this->scope = $scope; | |
| 114 | - $this->error_count = $error_count; | |
| 117 | + $this->access_token = $access_token; | |
| 118 | + $this->expires_at = $expires_at; | |
| 119 | + $this->token_type = $token_type; | |
| 120 | + $this->refresh_token = $refresh_token; | |
| 121 | + $this->id_token = $id_token; | |
| 122 | + $this->scope = $scope; | |
| 123 | + $this->error_count = $error_count; | |
| 124 | + $this->resource_indicator = ( $resource_indicator ?? new Resource_Indicator( null ) ); | |
| 115 | 125 | } |
| 116 | 126 | |
| 117 | 127 | /** |
| 118 | 128 | * Returns the access token. |
| @@ -193,8 +203,37 @@ | ||
| 193 | 203 | return $this->error_count; |
| 194 | 204 | } |
| 195 | 205 | |
| 196 | 206 | /** |
| 207 | + * Returns the RFC 8707 resource indicator this token was minted for. | |
| 208 | + * | |
| 209 | + * @return Resource_Indicator | |
| 210 | + */ | |
| 211 | + public function get_resource_indicator(): Resource_Indicator { | |
| 212 | + return $this->resource_indicator; | |
| 213 | + } | |
| 214 | + | |
| 215 | + /** | |
| 216 | + * Returns a new Token_Set bound to the given resource indicator. | |
| 217 | + * | |
| 218 | + * @param Resource_Indicator $resource_indicator The resource indicator. | |
| 219 | + * | |
| 220 | + * @return self | |
| 221 | + */ | |
| 222 | + public function with_resource_indicator( Resource_Indicator $resource_indicator ): self { | |
| 223 | + return new self( | |
| 224 | + $this->access_token, | |
| 225 | + $this->expires_at, | |
| 226 | + $this->token_type, | |
| 227 | + $this->refresh_token, | |
| 228 | + $this->id_token, | |
| 229 | + $this->scope, | |
| 230 | + $this->error_count, | |
| 231 | + $resource_indicator, | |
| 232 | + ); | |
| 233 | + } | |
| 234 | + | |
| 235 | + /** | |
| 197 | 236 | * Returns a new Token_Set with an incremented error count. |
| 198 | 237 | * |
| 199 | 238 | * @return self |
| 200 | 239 | */ |
| @@ -206,8 +245,9 @@ | ||
| 206 | 245 | $this->refresh_token, |
| 207 | 246 | $this->id_token, |
| 208 | 247 | $this->scope, |
| 209 | 248 | $this->error_count + 1, |
| 249 | + $this->resource_indicator, | |
| 210 | 250 | ); |
| 211 | 251 | } |
| 212 | 252 | |
| 213 | 253 | /** |
| @@ -227,15 +267,16 @@ | ||
| 227 | 267 | * @return array<string, string|int|null> The token set as an array. |
| 228 | 268 | */ |
| 229 | 269 | public function to_array(): array { |
| 230 | 270 | return [ |
| 231 | - 'access_token' => $this->access_token, | |
| 232 | - 'expires_at' => $this->expires_at, | |
| 233 | - 'token_type' => $this->token_type, | |
| 234 | - 'refresh_token' => $this->refresh_token, | |
| 235 | - 'id_token' => $this->id_token, | |
| 236 | - 'scope' => $this->scope, | |
| 237 | - 'error_count' => $this->error_count, | |
| 271 | + 'access_token' => $this->access_token, | |
| 272 | + 'expires_at' => $this->expires_at, | |
| 273 | + 'token_type' => $this->token_type, | |
| 274 | + 'refresh_token' => $this->refresh_token, | |
| 275 | + 'id_token' => $this->id_token, | |
| 276 | + 'scope' => $this->scope, | |
| 277 | + 'error_count' => $this->error_count, | |
| 278 | + 'resource_indicator' => $this->resource_indicator->value(), | |
| 238 | 279 | ]; |
| 239 | 280 | } |
| 240 | 281 | |
| 241 | 282 | /** |
| @@ -245,8 +286,10 @@ | ||
| 245 | 286 | * |
| 246 | 287 | * @return self |
| 247 | 288 | */ |
| 248 | 289 | public static function from_array( array $data ): self { |
| 290 | + $stored_indicator = ( $data['resource_indicator'] ?? null ); | |
| 291 | + | |
| 249 | 292 | return new self( |
| 250 | 293 | (string) ( $data['access_token'] ?? '' ), |
| 251 | 294 | (int) ( $data['expires_at'] ?? 0 ), |
| 252 | 295 | ( $data['token_type'] ?? Auth_Token_Type::DPOP ), |
| @@ -253,13 +296,20 @@ | ||
| 253 | 296 | ( $data['refresh_token'] ?? null ), |
| 254 | 297 | ( $data['id_token'] ?? null ), |
| 255 | 298 | ( $data['scope'] ?? null ), |
| 256 | 299 | (int) ( $data['error_count'] ?? 0 ), |
| 300 | + new Resource_Indicator( ( \is_string( $stored_indicator ) && $stored_indicator !== '' ) ? $stored_indicator : null ), | |
| 257 | 301 | ); |
| 258 | 302 | } |
| 259 | 303 | |
| 260 | 304 | /** |
| 261 | 305 | * Creates a Token_Set from a token endpoint response. |
| 306 | + * | |
| 307 | + * Per RFC 8707 §3 the AS may echo a `resource` field to confirm the audience | |
| 308 | + * it minted the token for. The spec does not require the client to honour | |
| 309 | + * the echo, and trusting an unverified echo into storage could later violate | |
| 310 | + * §2 on refresh. We deliberately ignore the echoed field; the caller is | |
| 311 | + * expected to stamp the requested indicator via with_resource_indicator(). | |
| 262 | 312 | * |
| 263 | 313 | * @param array<string, string|int|null> $response The parsed JSON response from the token endpoint. |
| 264 | 314 | * |
| 265 | 315 | * @return self |