$response */ public static function fromProviderResponse(array $response): self { if (! isset($response['access_token'])) { throw new \InvalidArgumentException('Provider response missing access_token'); } $expiresIn = (int) ($response['expires_in'] ?? 3600); return new self( accessToken: AccessToken::fromProviderResponse( $response['access_token'], $expiresIn ), refreshToken: isset($response['refresh_token']) ? RefreshToken::create($response['refresh_token']) : null, tokenType: TokenType::fromString($response['token_type'] ?? 'Bearer'), scope: isset($response['scope']) && ! empty($response['scope']) ? TokenScope::fromString($response['scope']) : null, ); } /** * Check if token is expired */ public function isExpired(): bool { return $this->accessToken->isExpired(); } /** * Check if token is still valid */ public function isValid(): bool { return $this->accessToken->isValid(); } /** * Check if token can be refreshed */ public function canRefresh(): bool { return $this->refreshToken !== null; } /** * Get authorization header value */ public function getAuthorizationHeader(): string { return $this->tokenType->getHeaderPrefix() . ' ' . $this->accessToken->toString(); } /** * Get seconds until expiration */ public function getSecondsUntilExpiration(): int { return $this->accessToken->getSecondsUntilExpiration(); } /** * Get expiration timestamp */ public function getExpiresAt(): Timestamp { return $this->accessToken->getExpiresAt(); } /** * Check if token has specific scope */ public function hasScope(string $scope): bool { return $this->scope !== null && $this->scope->includes($scope); } /** * Check if token has all required scopes * * @param array $requiredScopes */ public function hasScopes(array $requiredScopes): bool { return $this->scope !== null && $this->scope->includesAll($requiredScopes); } /** * Create new token with refreshed access token */ public function withRefreshedAccessToken( AccessToken $accessToken, ?RefreshToken $refreshToken = null ): self { return new self( accessToken: $accessToken, refreshToken: $refreshToken ?? $this->refreshToken, tokenType: $this->tokenType, scope: $this->scope, ); } /** * Convert to array for storage * * @return array */ public function toArray(): array { $array = [ 'access_token' => $this->accessToken->toString(), 'expires_at' => $this->accessToken->getExpiresAt()->toTimestamp(), 'token_type' => $this->tokenType->value, ]; if ($this->refreshToken !== null) { $array['refresh_token'] = $this->refreshToken->toString(); } if ($this->scope !== null) { $array['scope'] = $this->scope->toString(); } return $array; } /** * Convert to array for logging (with masked tokens) * * @return array */ public function toArrayMasked(): array { return [ 'access_token' => $this->accessToken->getMasked(), 'refresh_token' => $this->refreshToken?->getMasked(), 'expires_at' => $this->accessToken->getExpiresAt()->format('Y-m-d H:i:s'), 'expires_in' => $this->getSecondsUntilExpiration() . 's', 'token_type' => $this->tokenType->value, 'scope' => $this->scope?->toString(), 'is_expired' => $this->isExpired(), 'can_refresh' => $this->canRefresh(), ]; } /** * Create from stored array * * @param array $data */ public static function fromArray(array $data): self { if (! isset($data['access_token'], $data['expires_at'])) { throw new \InvalidArgumentException('Stored token data missing required fields'); } return new self( accessToken: AccessToken::create( $data['access_token'], Timestamp::fromTimestamp((int) $data['expires_at']) ), refreshToken: isset($data['refresh_token']) ? RefreshToken::create($data['refresh_token']) : null, tokenType: TokenType::fromString($data['token_type'] ?? 'Bearer'), scope: isset($data['scope']) && ! empty($data['scope']) ? TokenScope::fromString($data['scope']) : null, ); } }