42 Commits

Author SHA1 Message Date
AUDUL
344c96aeaf Merge branch 'main' into update-code-php8 2026-02-25 14:11:20 +01:00
loic
db5789d4d2 fix private email 2026-02-25 14:09:00 +01:00
AUDUL
f4ee42b17c Update code php8 (#4)
* update code to php 8
* remove lcobucci/jwt
2026-02-19 15:48:58 +01:00
loic
6d17ab2e2d update code to php 8
remove lcobucci/jwt
2026-02-19 15:46:54 +01:00
loic
31ece38879 update code to php 8
remove lcobucci/jwt
2026-02-19 15:45:56 +01:00
loic
7a03f29cc5 update code to php 8
remove lcobucci/jwt
2026-02-19 15:45:47 +01:00
loic
23a643494f update code to php 8
remove lcobucci/jwt
2026-02-19 15:41:16 +01:00
loic
e645a8afa2 update code to php 8
remove lcobucci/jwt
2026-02-19 15:40:07 +01:00
loic
fdcae826ed update code to php 8
remove lcobucci/jwt
2026-02-19 15:37:55 +01:00
loic
50fe6079e3 update code to php 8
remove lcobucci/jwt
2026-02-19 15:33:03 +01:00
loic
4b913d776d update code to php 8
remove lcobucci/jwt
2026-02-19 15:32:25 +01:00
loic
6952cbec6d update code to php 8
remove lcobucci/jwt
2026-02-19 15:30:16 +01:00
loic
ba795803bb update code to php 8
remove lcobucci/jwt
2026-02-19 15:29:43 +01:00
loic
a72cfeba83 update code to php 8
remove lcobucci/jwt
2026-02-19 15:29:01 +01:00
loic
831c8e0e25 update code to php 8
remove lcobucci/jwt
2026-02-19 15:27:56 +01:00
loic
942a875dc6 update code to php 8
remove lcobucci/jwt
2026-02-19 15:20:18 +01:00
AUDUL
e6626c4575 change naming (#2) 2026-02-19 11:58:15 +01:00
AUDUL
3f0f659741 fix firebase/php-jwt version (#1)
* fix firebase/php-jwt version

* add test for php8.5

* remove compatibility for some PHP versions
2026-02-19 11:48:28 +01:00
Matthew Grasmick
badae8f295 Add error_description to exception message (#57)
* Include error_description in exception message
2024-09-17 19:07:23 +02:00
Patrick Bußmann
5d3bd66b5b Updated changelog for 0.3.0 2024-05-18 00:39:27 +02:00
Patrick Bußmann
6b38a21212 fix: KeyDumpSigner for PHP < 8 2024-05-18 00:35:08 +02:00
Richard van Velzen
d5048c7f76 Allow lcobucci/jwt ^5.0 (#44) 2024-05-17 20:57:52 +02:00
Stefano Rosanelli
74818d3854 Fix: handle different JWT::decode signatures (#54)
* fix: handle differente JWT::decode signatures
* chore: coding style
* chore: code style again :)
2024-05-17 20:55:59 +02:00
Patrick Bußmann
561ae0f92c Updated changelog 2022-10-01 13:10:37 +02:00
Patrick
cb1bf60335 add sub to resource owner toArray() (#38) 2022-10-01 13:06:35 +02:00
Patrick
16c3708cf8 Fix Apple key retrieval when using Guzzle logging (#39)
When using Guzzle logging to log a message body the stream is not reset to
the start, causing future calls to getContents() to be empty. It seems
recommended way of getting the body as a string is to use `__toString()`
(https://github.com/guzzle/guzzle/pull/1842)
2022-10-01 13:06:24 +02:00
Patrick Bußmann
d9d17976f1 Added method for revoking access and refresh tokens 2022-07-09 17:44:20 +02:00
Patrick Bußmann
3a4576b801 Fixed issue with JWT 5 and supported methods 2022-05-10 00:09:28 +02:00
Patrick Bußmann
51c96fba61 Updated changelog for new version 2022-04-29 23:04:42 +02:00
Patrick Bußmann
a317e52085 Made older PHP Versions working 2022-04-29 22:57:56 +02:00
Daniel
415e29753b Support for firebase/php-jwt Version 6 (#32)
* wip

* wip

* wip
2022-04-29 22:57:02 +02:00
Patrick Bußmann
61e5f98312 Moved to GitHub Actions and increased test coverage 2021-08-25 21:47:57 +02:00
Thijs-jan Veldhuizen
5f20997c99 Moved fetching Apple JWT keys from token to provider
Since AccessToken is serialized in specific set-ups, existence of the httpClient leads to problems, since closures are not serializable. Also, it's not necessary to keep the httpClient there, since it's only used in the constructor.

Fixes #26, #28
2021-08-03 11:05:27 +02:00
Patrick Bußmann
f6fe8c0b1b Updated CHANGELOG 2021-03-14 23:14:36 +01:00
Patrick Bußmann
113f72fa62 Merge pull request #25 from tjveldhuizen/fix-bc-break-lcobucci-jwt
Fix BC-break for combination of PHP 7.4 and lcobucci/jwt 3.4
2021-03-08 09:13:31 +01:00
Thijs-jan Veldhuizen
eb4d697dda Fix BC-break for combination of PHP 7.4 and lcobucci/jwt 3.4
Fixes #24
2021-02-17 23:47:58 +01:00
Patrick Bußmann
95cc38cd0c Removing microseconds so that Sign in with Apple works again 2021-01-17 23:19:45 +01:00
Patrick Bußmann
a08ded7b3a Added Codecov and updated changelog 2021-01-17 17:46:58 +01:00
Patrick Bußmann
31c61d334b Increased library compatibility and fixed all tests 2021-01-17 17:30:28 +01:00
Patrick Bußmann
d649df8313 Merge pull request #20 from zhangv/master
support Lcobucci-jwt v3.4+
2021-01-15 12:11:42 +01:00
Patrick Bußmann
6702931e2e Merge branch 'master' into master 2021-01-15 12:11:32 +01:00
zhangv
06ef0a50d1 support Lcobucci-jwt v3.4+
and compatible to the old versions
2020-12-11 14:45:06 +08:00
16 changed files with 744 additions and 500 deletions

View File

@@ -0,0 +1,78 @@
name: CI
on:
pull_request:
push:
branches:
- main
env:
COMPOSER_ROOT_VERSION: '1.99.99'
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: none
ini-values: memory_limit=-1
tools: composer:v2
- uses: ramsey/composer-install@v3
- name: 'Lint the PHP source code'
run: ./vendor/bin/parallel-lint src test
coding-standards:
name: Coding Standards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
coverage: none
ini-values: memory_limit=-1
tools: composer:v2
- uses: ramsey/composer-install@v3
- name: Check coding standards
run: ./vendor/bin/phpcs src --standard=psr2 -sp --colors
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.4'
- '8.5'
dependencies:
- lowest
- highest
experimental:
- false
include:
- php-version: '8.3'
experimental: false
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '${{ matrix.php-version }}'
coverage: pcov
ini-values: memory_limit=-1
tools: composer:v2
- name: Prepare for tests
run: mkdir -p build/logs
- uses: ramsey/composer-install@v3
with:
dependency-versions: '${{ matrix.dependencies }}'
composer-options: '${{ matrix.composer-options }}'
- name: Run unit tests
run: ./vendor/bin/phpunit

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
composer.phar
composer.lock
.DS_Store
.phpunit.result.cache

View File

@@ -1,52 +0,0 @@
language: php
matrix:
include:
- php: 5.6
- php: 7.0
- php: 7.1
- php: 7.2
- php: nightly
- php: hhvm-3.6
sudo: required
dist: trusty
group: edge
- php: hhvm-3.9
sudo: required
dist: trusty
group: edge
- php: hhvm-3.12
sudo: required
dist: trusty
group: edge
- php: hhvm-3.15
sudo: required
dist: trusty
group: edge
- php: hhvm-nightly
sudo: required
dist: trusty
group: edge
fast_finish: true
allow_failures:
- php: nightly
- php: hhvm-3.6
- php: hhvm-3.9
- php: hhvm-3.12
- php: hhvm-3.15
- php: hhvm-nightly
- php: 7.3
- php: 7.4
before_script:
- travis_retry composer self-update
- travis_retry composer install --no-interaction --prefer-source --dev
- travis_retry phpenv rehash
script:
- ./vendor/bin/phpcs --standard=psr2 src/
- ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
after_script:
- if [ "$TRAVIS_PHP_VERSION" == "7.1" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi
- if [ "$TRAVIS_PHP_VERSION" == "7.1" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi

View File

@@ -1,7 +1,7 @@
# Changelog
All Notable changes to `oauth2-apple` will be documented in this file
## 0.3.0 - 202X-XX-XX
## 0.5.0 - 202X-XX-XX
### Added
- Nothing
@@ -18,6 +18,68 @@ All Notable changes to `oauth2-apple` will be documented in this file
### Security
- Nothing
## 0.4.0 - 2026-02-19
### Added
- Allow firebase/php-jwt ^7.0 [#1](https://github.com/code-rhapsodie/oauth2-apple/pull/1)
- Tests for php 8.5 [#1](https://github.com/code-rhapsodie/oauth2-apple/pull/1)
### Removed
- Compatibility with PHP 5.6 - 7.0 - 7.1 - 7.2 - 7.3 - 7.4
## 0.3.0 - 2024-05-18
### Added
- Allow lcobucci/jwt ^5.0 [#44](https://github.com/patrickbussmann/oauth2-apple/pull/44)
### Fixed
- Handle different JWT::decode signatures [#54](https://github.com/patrickbussmann/oauth2-apple/pull/54)
## 0.2.10 - 2022-10-01
### Added
- "sub" to Resource Owner->toArray() [#38](https://github.com/patrickbussmann/oauth2-apple/pull/38)
- Apple Key retrieval when using Guzzle Logging [#39](https://github.com/patrickbussmann/oauth2-apple/pull/39)
## 0.2.9 - 2022-07-09
### Added
- Method for revoking access and refresh tokens [#37](https://github.com/patrickbussmann/oauth2-apple/issues/37)
## 0.2.8 - 2022-05-10
### Fixed
- Issue with firebase/php-jwt v5 [#34](https://github.com/patrickbussmann/oauth2-apple/issues/34) (thanks to [tjveldhuizen](https://github.com/tjveldhuizen))
## 0.2.7 - 2022-04-29
### Added
- Support for firebase/php-jwt v6 [#31](https://github.com/patrickbussmann/oauth2-apple/pull/31) (thanks to [bashgeek](https://github.com/bashgeek))
## 0.2.6 - 2021-08-25
### Added
- GitHub Actions CI
### Removed
- Travis CI
### Fixed
- Fixed bug with serialization of AppleAccessToken [#29](https://github.com/patrickbussmann/oauth2-apple/pull/29) (thanks to [tjveldhuizen](https://github.com/tjveldhuizen))
## 0.2.5 - 2021-03-10
### Fixed
- Fix BC-break for combination of PHP 7.4 and lcobucci/jwt 3.4 [#25](https://github.com/patrickbussmann/oauth2-apple/pull/25) (thanks to [tjveldhuizen](https://github.com/tjveldhuizen))
## 0.2.4 - 2021-01-17
### Added
- Codecov for Code Coverage
### Fixed
- Few compatibility issues with PHP 8 and PHP 5.6 (Read [#16](https://github.com/patrickbussmann/oauth2-apple/pull/16) for more details)
## 0.2.3 - 2021-01-05
### Added

View File

@@ -2,7 +2,7 @@
Contributions are **welcome** and will be fully **credited**.
We accept contributions via Pull Requests on [Github](https://github.com/patrickbussmann/oauth2-apple).
We accept contributions via Pull Requests on [Github](https://github.com/code-rhapsodie/oauth2-apple).
## Pull Requests

View File

@@ -1,13 +1,12 @@
# Sign in with Apple ID Provider for OAuth 2.0 Client
[![Latest Version](https://img.shields.io/github/release/patrickbussmann/oauth2-apple.svg?style=flat-square)](https://github.com/patrickbussmann/oauth2-apple/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Build Status](https://img.shields.io/travis/patrickbussmann/oauth2-apple/master.svg?style=flat-square)](https://travis-ci.org/patrickbussmann/oauth2-apple)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/patrickbussmann/oauth2-apple.svg?style=flat-square)](https://scrutinizer-ci.com/g/patrickbussmann/oauth2-apple/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/patrickbussmann/oauth2-apple.svg?style=flat-square)](https://scrutinizer-ci.com/g/patrickbussmann/oauth2-apple)
[![Total Downloads](https://img.shields.io/packagist/dt/patrickbussmann/oauth2-apple.svg?style=flat-square)](https://packagist.org/packages/patrickbussmann/oauth2-apple)
[![Latest Version](https://img.shields.io/github/release/code-rhapsodie/oauth2-apple.svg?style=flat-square)](https://github.com/code-rhapsodie/oauth2-apple/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Total Downloads](https://img.shields.io/packagist/dt/code-rhapsodie/oauth2-apple.svg?style=flat-square)](https://packagist.org/packages/code-rhapsodie/oauth2-apple)
This package provides Apple ID OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client).
This package is a fork of [patrickbussmann/oauth2-apple](https://github.com/patrickbussmann/oauth2-apple)
## Before You Begin
Here you can find the official Apple documentation:
@@ -22,7 +21,7 @@ Maybe Apple changes this sometime.
To install, use composer:
```
composer require patrickbussmann/oauth2-apple
composer require code-rhapsodie/oauth2-apple
```
## Usage
@@ -87,6 +86,34 @@ if (!isset($_POST['code'])) {
}
```
### Revoke Code Flow
```php
// $leeway is needed for clock skew
Firebase\JWT\JWT::$leeway = 60;
$provider = new League\OAuth2\Client\Provider\Apple([
'clientId' => '{apple-client-id}',
'teamId' => '{apple-team-id}', // 1A234BFK46 https://developer.apple.com/account/#/membership/ (Team ID)
'keyFileId' => '{apple-key-file-id}', // 1ABC6523AA https://developer.apple.com/account/resources/authkeys/list (Key ID)
'keyFilePath' => '{apple-key-file-path}', // __DIR__ . '/AuthKey_1ABC6523AA.p8' -> Download key above
'redirectUri' => 'https://example.com/callback-url',
]);
$token = $token->getToken(); // Use the token of "Authorization Code Flow" which you saved somewhere for the user
try {
$provider->revokeAccessToken($token /*, 'access_token' or 'refresh_token' */);
// Successfully revoked the token!
} catch (Exception $e) {
// Failed to revoke
exit(':-(');
}
```
### Managing Scopes
When creating your Apple authorization URL, you can specify the state and scopes your application may authorize.
@@ -129,15 +156,15 @@ $ ./vendor/bin/phpunit
## Contributing
Please see [CONTRIBUTING](https://github.com/patrickbussmann/oauth2-apple/blob/master/CONTRIBUTING.md) for details.
Please see [CONTRIBUTING](https://github.com/code-rhapsodie/oauth2-apple/blob/main/CONTRIBUTING.md) for details.
## Credits
- [All Contributors](https://github.com/patrickbussmann/oauth2-apple/contributors)
- [All Contributors](https://github.com/code-rhapsodie/oauth2-apple/contributors)
Template for this repository was the [LinkedIn](https://github.com/thephpleague/oauth2-linkedin).
## License
The MIT License (MIT). Please see [License File](https://github.com/patrickbussmann/oauth2-apple/blob/master/LICENSE) for more information.
The MIT License (MIT). Please see [License File](https://github.com/code-rhapsodie/oauth2-apple/blob/main/LICENSE) for more information.

View File

@@ -1,5 +1,5 @@
{
"name": "patrickbussmann/oauth2-apple",
"name": "code-rhapsodie/oauth2-apple",
"description": "Sign in with Apple OAuth 2.0 Client Provider for The PHP League OAuth2-Client",
"license": "MIT",
"authors": [
@@ -9,6 +9,9 @@
"homepage": "https://github.com/patrickbussmann"
}
],
"replace": {
"patrickbussmann/oauth2-apple": "*"
},
"keywords": [
"oauth",
"oauth2",
@@ -19,16 +22,17 @@
"sign-in-with-apple"
],
"require": {
"league/oauth2-client": "^2.0",
"php": ">=8.1",
"ext-json": "*",
"firebase/php-jwt": "^5.2",
"lcobucci/jwt": "~3.3.3"
"league/oauth2-client": "^2.0",
"firebase/php-jwt": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^7.5",
"mockery/mockery": "~1.3.3",
"squizlabs/php_codesniffer": "~2.0",
"ext-json": "*"
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"mockery/mockery": "^1.3",
"php-parallel-lint/php-parallel-lint": "^1.3",
"squizlabs/php_codesniffer": "^3.0 || ^4.0",
"composer/semver": "^3.0"
},
"autoload": {
"psr-4": {
@@ -42,7 +46,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "0.3.x-dev"
"dev-master": "0.4.x-dev"
}
}
}

View File

@@ -1,34 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
>
<logging>
<log type="coverage-html"
target="./build/coverage/html"
lowUpperBound="35"
highLowerBound="70"/>
<log type="coverage-clover"
target="./build/coverage/log/coverage.xml"/>
</logging>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false"
convertDeprecationsToExceptions="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true"
convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false"
stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd">
<logging/>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./test/</directory>
<exclude>./test/src/Provider/TestApple.php</exclude>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./test</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@@ -1,18 +1,21 @@
<?php
declare(strict_types=1);
namespace League\OAuth2\Client\Provider;
use Exception;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use InvalidArgumentException;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Provider\Exception\AppleAccessDeniedException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Token\AccessTokenInterface;
use League\OAuth2\Client\Token\AppleAccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class Apple extends AbstractProvider
@@ -22,30 +25,27 @@ class Apple extends AbstractProvider
/**
* Default scopes
*
* @var array
* @var array<string>
*/
public $defaultScopes = ['name', 'email'];
public array $defaultScopes = ['name', 'email'];
/**
* @var string the team id
* the team id
*/
protected $teamId;
protected string $teamId;
/**
* @var string the key file id
* the key file id
*/
protected $keyFileId;
protected string $keyFileId;
/**
* @var string the key file path
* the key file path
*/
protected $keyFilePath;
protected string $keyFilePath;
/**
* Constructs Apple's OAuth 2.0 service provider.
*
* @param array $options
* @param array $collaborators
* @inheritDoc
*/
public function __construct(array $options = [], array $collaborators = [])
{
@@ -65,50 +65,49 @@ class Apple extends AbstractProvider
}
/**
* Creates an access token from a response.
*
* The grant that was used to fetch the response can be used to provide
* additional context.
*
* @param array $response
* @param AbstractGrant $grant
* @return AccessTokenInterface
* @inheritDoc
*/
protected function createAccessToken(array $response, AbstractGrant $grant)
protected function createAccessToken(array $response, AbstractGrant $grant): AccessTokenInterface
{
return new AppleAccessToken($this->getHttpClient(), $response);
return new AppleAccessToken($this->getAppleKeys(), $response);
}
/**
* Get the string used to separate scopes.
*
* @return string
* @return array<string, Key> Apple's JSON Web Keys
*/
protected function getScopeSeparator()
private function getAppleKeys(): array
{
$response = $this->httpClient->request('GET', 'https://appleid.apple.com/auth/keys');
if ($response->getStatusCode() === 200) {
return JWK::parseKeySet(json_decode($response->getBody()->__toString(), true));
}
return [];
}
/**
* @inheritDoc
*/
protected function getScopeSeparator(): string
{
return ' ';
}
/**
* Change response mode when scope requires it
*
* @param array $options
*
* @return array
* @inheritDoc
*/
protected function getAuthorizationParameters(array $options)
protected function getAuthorizationParameters(array $options): array
{
$options = parent::getAuthorizationParameters($options);
if (strpos($options['scope'], 'name') !== false || strpos($options['scope'], 'email') !== false) {
if (str_contains($options['scope'], 'name') || str_contains($options['scope'], 'email')) {
$options['response_mode'] = 'form_post';
}
return $options;
}
/**
* @param AccessToken $token
*
* @return mixed
* @inheritDoc
*/
protected function fetchResourceOwnerDetails(AccessToken $token)
{
@@ -117,47 +116,43 @@ class Apple extends AbstractProvider
}
/**
* Get authorization url to begin OAuth flow
*
* @return string
* @inheritDoc
*/
public function getBaseAuthorizationUrl()
public function getBaseAuthorizationUrl(): string
{
return 'https://appleid.apple.com/auth/authorize';
}
/**
* Get access token url to retrieve token
*
* @return string
* @inheritDoc
*/
public function getBaseAccessTokenUrl(array $params)
public function getBaseAccessTokenUrl(array $params): string
{
return 'https://appleid.apple.com/auth/token';
}
/**
* Get revoke token url to revoke token
*/
public function getBaseRevokeTokenUrl(): string
{
return 'https://appleid.apple.com/auth/revoke';
}
/**
* Get provider url to fetch user details
*
* @param AccessToken $token
*
* @return string
* @throws Exception
* @inheritDoc
*/
public function getResourceOwnerDetailsUrl(AccessToken $token)
public function getResourceOwnerDetailsUrl(AccessToken $token): string
{
throw new Exception('No Apple ID REST API available yet!');
}
/**
* Get the default scopes used by this provider.
*
* This should not be a complete list of all scopes, but the minimum
* required for the provider user interface!
*
* @return array
* @inheritDoc
*/
protected function getDefaultScopes()
protected function getDefaultScopes(): array
{
return $this->defaultScopes;
}
@@ -165,16 +160,21 @@ class Apple extends AbstractProvider
/**
* Check a provider response for errors.
*
* @param ResponseInterface $response
* @param array $data Parsed response data
* @return void
* @inheritDoc
* @throws AppleAccessDeniedException
*/
protected function checkResponse(ResponseInterface $response, $data)
protected function checkResponse(ResponseInterface $response, $data): void
{
if ($response->getStatusCode() >= 400) {
$message = $response->getReasonPhrase();
if (array_key_exists('error', $data)) {
$message = $data['error'];
}
if (array_key_exists('error_description', $data)) {
$message .= ': ' . $data['error_description'];
}
throw new AppleAccessDeniedException(
array_key_exists('error', $data) ? $data['error'] : $response->getReasonPhrase(),
$message,
array_key_exists('code', $data) ? $data['code'] : $response->getStatusCode(),
$response
);
@@ -183,19 +183,15 @@ class Apple extends AbstractProvider
/**
* Generate a user object from a successful user details request.
*
* @param array $response
* @param AccessToken $token
* @return AppleResourceOwner
*/
protected function createResourceOwner(array $response, AccessToken $token)
protected function createResourceOwner(array $response, AccessToken $token): AppleResourceOwner
{
return new AppleResourceOwner(
array_merge(
['sub' => $token->getResourceOwnerId()],
$response,
[
'email' => isset($token->getValues()['email'])
? $token->getValues()['email'] : (isset($response['email']) ? $response['email'] : null),
'email' => $token->getValues()['email'] ?? ($response['email'] ?? null),
'isPrivateEmail' => $token instanceof AppleAccessToken ? $token->isPrivateEmail() : null
]
),
@@ -206,34 +202,92 @@ class Apple extends AbstractProvider
/**
* {@inheritDoc}
*/
public function getAccessToken($grant, array $options = [])
public function getAccessToken($grant, array $options = []): AccessTokenInterface
{
$signer = new Sha256();
$time = new \DateTimeImmutable();
$expiresAt = $time->modify('+1 Hour');
$time = time();
$token = (new Builder())
->issuedBy($this->teamId)
->permittedFor('https://appleid.apple.com')
->issuedAt($time->getTimestamp())
->expiresAt($expiresAt->getTimestamp())
->relatedTo($this->clientId)
->withHeader('alg', 'ES256')
->withHeader('kid', $this->keyFileId)
->getToken($signer, $this->getLocalKey());
$payload = [
'iss' => $this->teamId,
'iat' => $time,
'exp' => $time + 3600,
'aud' => 'https://appleid.apple.com',
'sub' => $this->clientId,
];
$jwt = JWT::encode(
$payload,
$this->getLocalKey(),
'ES256',
$this->keyFileId,
[
'kid' => $this->keyFileId,
'alg' => 'ES256',
]
);
$options += [
'client_secret' => (string) $token
'client_secret' => $jwt
];
return parent::getAccessToken($grant, $options);
}
/**
* @return Key
* Revokes an access or refresh token using a specified token.
*/
public function getLocalKey()
public function revokeAccessToken(string $token, ?string $tokenTypeHint = null)
{
return new Key('file://' . $this->keyFilePath);
$time = time();
$payload = [
'iss' => $this->teamId,
'iat' => $time,
'exp' => $time + 3600,
'aud' => 'https://appleid.apple.com',
'sub' => $this->clientId,
];
$clientSecret = JWT::encode(
$payload,
$this->getLocalKey(),
'ES256',
$this->keyFileId,
[
'kid' => $this->keyFileId,
'alg' => 'ES256',
]
);
$params = [
'client_id' => $this->clientId,
'client_secret' => $clientSecret,
'token' => $token
];
if ($tokenTypeHint !== null) {
$params += [
'token_type_hint' => $tokenTypeHint
];
}
$method = $this->getAccessTokenMethod();
$url = $this->getBaseRevokeTokenUrl();
if (property_exists($this, 'optionProvider')) {
$options = $this->optionProvider->getAccessTokenOptions(self::METHOD_POST, $params);
} else {
$options = $this->getAccessTokenOptions($params);
}
$request = $this->getRequest($method, $url, $options);
return $this->getParsedResponse($request);
}
public function getLocalKey(): string
{
if (!file_exists($this->keyFilePath)) {
throw new \InvalidArgumentException('Could not read key file');
}
return file_get_contents($this->keyFilePath);
}
}

View File

@@ -1,106 +1,72 @@
<?php namespace League\OAuth2\Client\Provider;
<?php
declare(strict_types=1);
namespace League\OAuth2\Client\Provider;
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
/**
* @property array $response
* @property string $uid
*/
class AppleResourceOwner extends GenericResourceOwner
{
use ArrayAccessorTrait;
/**
* Raw response
*
* @var array
*/
protected $response = [];
/**
* @var string|null
*/
private $email;
private ?string $email;
/**
* @var boolean true when its private relay from apple else the user mail address
* true when it's a private relay from apple else the user mail address
*/
private $isPrivateEmail;
private bool $isPrivateEmail;
/**
* Gets resource owner attribute by key. The key supports dot notation.
*
* @param string $key
*
* @return mixed
*/
public function getAttribute($key)
public function getAttribute(string $key): mixed
{
return $this->getValueByKey($this->response, (string) $key);
return $this->getValueByKey($this->response, $key);
}
/**
* Get user first name
*
* @return string|null
*/
public function getFirstName()
public function getFirstName(): ?string
{
$name = $this->getAttribute('name');
if (isset($name)) {
if (is_array($name)) {
return $name['firstName'];
}
return null;
}
/**
* Get user user id
*
* @return string|null
* @inheritDoc
*/
public function getId()
{
return $this->resourceOwnerId;
}
/**
* Get user last name
*
* @return string|null
*/
public function getLastName()
public function getLastName(): ?string
{
$name = $this->getAttribute('name');
if (isset($name)) {
if (is_array($name)) {
return $name['lastName'];
}
return null;
}
/**
* Get user email, if available
*
* @return string|null
*/
public function getEmail()
public function getEmail(): ?string
{
return $this->getAttribute('email');
}
/**
* @return bool
*/
public function isPrivateEmail()
public function isPrivateEmail(): bool
{
return (bool) $this->getAttribute('isPrivateEmail');
}
/**
* Return all of the owner details available as an array.
*
* @return array
* @inheritDoc
*/
public function toArray()
public function toArray(): array
{
return $this->response;
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace League\OAuth2\Client\Provider\Exception;
class AppleAccessDeniedException extends IdentityProviderException

View File

@@ -2,58 +2,43 @@
namespace League\OAuth2\Client\Token;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use GuzzleHttp\ClientInterface;
use Firebase\JWT\Key;
use InvalidArgumentException;
class AppleAccessToken extends AccessToken
{
/**
* @var string
*/
protected $idToken;
protected string $idToken;
protected string $email;
protected ?bool $isPrivateEmail = null;
/**
* @var string
*/
protected $email;
/**
* @var boolean
*/
protected $isPrivateEmail;
/**
* @var ClientInterface
*/
protected $httpClient;
/**
* Constructs an access token.
*
* @param ClientInterface $httpClient the http client to use
* @param Key[] $keys Valid Apple JWT keys
* @param array $options An array of options returned by the service provider
* in the access token request. The `access_token` option is required.
* @throws InvalidArgumentException if `access_token` is not provided in `$options`.
*
* @throws \Exception
*/
public function __construct($httpClient, array $options = [])
public function __construct(array $keys, array $options = [])
{
$this->httpClient = $httpClient;
if (array_key_exists('refresh_token', $options)) {
if (empty($options['id_token'])) {
throw new InvalidArgumentException('Required option not passed: "id_token"');
}
$decoded = null;
$keys = $this->getAppleKey();
$last = end($keys);
foreach ($keys as $key) {
try {
$decoded = JWT::decode($options['id_token'], $key, ['RS256']);
try {
$decoded = JWT::decode($options['id_token'], $key);
} catch (\UnexpectedValueException) {
$headers = (object) ['alg' => 'RS256'];
$decoded = JWT::decode($options['id_token'], $key, $headers);
}
break;
} catch (\Exception $exception) {
if ($last === $key) {
@@ -61,9 +46,11 @@ class AppleAccessToken extends AccessToken
}
}
}
if (null === $decoded) {
throw new \Exception('Got no data within "id_token"!');
}
$payload = json_decode(json_encode($decoded), true);
$options['resource_owner_id'] = $payload['sub'];
@@ -80,48 +67,25 @@ class AppleAccessToken extends AccessToken
parent::__construct($options);
if (isset($options['id_token'])) {
$this->idToken = $options['id_token'];
$this->idToken = (string)$options['id_token'];
}
if (isset($options['email'])) {
$this->email = $options['email'];
$this->email = (string)$options['email'];
}
}
/**
* @return array Apple's JSON Web Key
*/
protected function getAppleKey()
{
$response = $this->httpClient->request('GET', 'https://appleid.apple.com/auth/keys');
if ($response) {
return JWK::parseKeySet(json_decode($response->getBody()->getContents(), true));
}
return false;
}
/**
* @return string
*/
public function getIdToken()
public function getIdToken(): string
{
return $this->idToken;
}
/**
* @return string
*/
public function getEmail()
public function getEmail(): string
{
return $this->email;
}
/**
* @return boolean
*/
public function isPrivateEmail()
public function isPrivateEmail(): ?bool
{
return $this->isPrivateEmail;
}

View File

@@ -2,30 +2,26 @@
namespace League\OAuth2\Client\Test\Provider;
use Exception;
use Firebase\JWT\JWT;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use InvalidArgumentException;
use Lcobucci\JWT\Builder;
use League\OAuth2\Client\Provider\Apple;
use League\OAuth2\Client\Test\Provider\TestApple;
use League\OAuth2\Client\Provider\AppleResourceOwner;
use League\OAuth2\Client\Provider\Exception\AppleAccessDeniedException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\QueryBuilderTrait;
use PHPUnit\Framework\TestCase;
use Mockery as m;
use PHPUnit\Framework\TestCase;
class AppleTest extends TestCase
{
use QueryBuilderTrait;
/** @var Apple|\Mockery\MockInterface */
protected $provider;
protected function setUp()
/**
* @return Apple
*/
private function getProvider()
{
$this->provider = new \League\OAuth2\Client\Provider\Apple([
return new Apple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
@@ -34,62 +30,49 @@ class AppleTest extends TestCase
]);
}
public function tearDown()
public function testMissingTeamIdDuringInstantiationThrowsException()
{
m::close();
parent::tearDown();
$this->expectException('InvalidArgumentException');
new Apple([
'clientId' => 'mock.example',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/p256-private-key.p8',
'redirectUri' => 'none'
]);
}
/**
* @expectedException InvalidArgumentException
*/
public function testMissingTeamIdDuringInstantiationThrowsException()
{
new \League\OAuth2\Client\Provider\Apple([
'clientId' => 'mock.example',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/p256-private-key.p8',
'redirectUri' => 'none'
]);
}
public function testMissingKeyFileIdDuringInstantiationThrowsException()
{
$this->expectException('InvalidArgumentException');
new Apple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFilePath' => __DIR__ . '/p256-private-key.p8',
'redirectUri' => 'none'
]);
}
/**
* @expectedException InvalidArgumentException
*/
public function testMissingKeyFileIdDuringInstantiationThrowsException()
{
new \League\OAuth2\Client\Provider\Apple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFilePath' => __DIR__ . '/p256-private-key.p8',
'redirectUri' => 'none'
]);
}
public function testMissingKeyFilePathDuringInstantiationThrowsException()
{
$this->expectException('InvalidArgumentException');
new Apple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'redirectUri' => 'none'
]);
}
/**
* @expectedException InvalidArgumentException
*/
public function testMissingKeyFilePathDuringInstantiationThrowsException()
{
new \League\OAuth2\Client\Provider\Apple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'redirectUri' => 'none'
]);
}
/**
* @expectedException InvalidArgumentException
*/
public function testMissingKeyDuringInstantiationThrowsException()
{
$this->provider->getLocalKey();
}
public function testMissingKeyDuringInstantiationThrowsException()
{
$this->expectException('InvalidArgumentException');
$this->getProvider()->getLocalKey();
}
public function testAuthorizationUrl()
{
$url = $this->provider->getAuthorizationUrl();
$provider = $this->getProvider();
$url = $provider->getAuthorizationUrl();
$uri = parse_url($url);
parse_str($uri['query'], $query);
@@ -99,22 +82,24 @@ class AppleTest extends TestCase
$this->assertArrayHasKey('scope', $query);
$this->assertArrayHasKey('response_type', $query);
$this->assertArrayHasKey('response_mode', $query);
$this->assertNotNull($this->provider->getState());
$this->assertNotNull($provider->getState());
}
public function testScopes()
{
$provider = $this->getProvider();
$scopeSeparator = ' ';
$options = ['scope' => [uniqid(), uniqid()]];
$query = ['scope' => implode($scopeSeparator, $options['scope'])];
$url = $this->provider->getAuthorizationUrl($options);
$url = $provider->getAuthorizationUrl($options);
$encodedScope = $this->buildQueryString($query);
$this->assertContains($encodedScope, $url);
$this->assertNotFalse(strpos($url, $encodedScope));
}
public function testGetAuthorizationUrl()
{
$url = $this->provider->getAuthorizationUrl();
$provider = $this->getProvider();
$url = $provider->getAuthorizationUrl();
$uri = parse_url($url);
$this->assertEquals('/auth/authorize', $uri['path']);
@@ -122,140 +107,282 @@ class AppleTest extends TestCase
public function testGetBaseAccessTokenUrl()
{
$provider = $this->getProvider();
$params = [];
$url = $this->provider->getBaseAccessTokenUrl($params);
$url = $provider->getBaseAccessTokenUrl($params);
$uri = parse_url($url);
$this->assertEquals('/auth/token', $uri['path']);
}
/**
* @expectedException \Firebase\JWT\SignatureInvalidException
*/
public function testGetAccessToken()
{
$provider = new TestApple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/../../resources/p256-private-key.p8',
'redirectUri' => 'none'
]);
$this->expectException('UnexpectedValueException');
$provider = new TestApple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/../../resources/p256-private-key.p8',
'redirectUri' => 'none'
]);
$provider = m::mock($provider);
$time = new \DateTimeImmutable();
$expiresAt = $time->modify('+1 Hour');
$token = (new Builder())
->issuedBy('test-team-id')
->permittedFor('https://appleid.apple.com')
->issuedAt($time->getTimestamp())
->expiresAt($expiresAt->getTimestamp())
->relatedTo('test-client')
->withClaim('sub', 'test')
->withHeader('alg', 'RS256')
->withHeader('kid', 'test')
->getToken();
$time = time();
$client = m::mock(ClientInterface::class);
$client->shouldReceive('request')
->times(1)
->andReturn(new Response(200, [], file_get_contents('https://appleid.apple.com/auth/keys')));
$client->shouldReceive('send')
->times(1)
->andReturn(new Response(200, [], json_encode([
'access_token' => 'aad897dee58fe4f66bf220c181adaf82b.0.mrwxq.hmiE0djj1vJqoNisKmF-pA',
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'r4a6e8b9c50104b78bc86b0d2649353fa.0.mrwxq.54joUj40j0cpuMANRtRjfg',
'id_token' => (string) $token
])));
$provider->setHttpClient($client);
$payload = [
'iss' => 'test-team-id',
'iat' => $time,
'exp' => $time + 3600,
'aud' => 'https://appleid.apple.com',
'sub' => 'test-client',
];
$provider->getAccessToken('authorization_code', [
'code' => 'hello-world'
]);
$jwt = JWT::encode(
$payload,
'file://' . __DIR__ . '/../private_key.pem',
'ES256',
'test',
[
'kid' => 'test',
'alg' => 'ES256',
]
);
$client = m::mock(ClientInterface::class);
$client->shouldReceive('request')
->times(1)
->andReturn(new Response(200, [], file_get_contents('https://appleid.apple.com/auth/keys')));
$client->shouldReceive('send')
->times(1)
->andReturn(new Response(200, [], json_encode([
'access_token' => 'aad897dee58fe4f66bf220c181adaf82b.0.mrwxq.hmiE0djj1vJqoNisKmF-pA',
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'r4a6e8b9c50104b78bc86b0d2649353fa.0.mrwxq.54joUj40j0cpuMANRtRjfg',
'id_token' => $jwt
])));
$provider->setHttpClient($client);
$provider->getAccessToken('authorization_code', [
'code' => 'hello-world'
]);
}
public function testFetchingOwnerDetails()
{
$class = new \ReflectionClass($this->provider);
$method = $class->getMethod('fetchResourceOwnerDetails');
$method->setAccessible(true);
public function testGetAccessTokenFailedBecauseAppleHasError()
{
$this->expectException('Exception');
$this->expectExceptionMessage('Got no data within "id_token"!');
$arr = [
'name' => 'John Doe'
];
$_POST['user'] = json_encode($arr);
$data = $method->invokeArgs($this->provider, [new AccessToken(['access_token' => 'hello'])]);
$provider = new TestApple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/../../resources/p256-private-key.p8',
'redirectUri' => 'none'
]);
$provider = m::mock($provider);
$this->assertEquals($arr, $data);
}
$client = m::mock(ClientInterface::class);
$client->shouldReceive('request')
->times(1)
->andReturn(new Response(500, [], 'Internal Server Error'));
$client->shouldReceive('send')
->times(1)
->andReturn(new Response(200, [], json_encode([
'access_token' => 'aad897dee58fe4f66bf220c181adaf82b.0.mrwxq.hmiE0djj1vJqoNisKmF-pA',
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'r4a6e8b9c50104b78bc86b0d2649353fa.0.mrwxq.54joUj40j0cpuMANRtRjfg',
'id_token' => 'abc'
])));
$provider->setHttpClient($client);
$provider->getAccessToken('authorization_code', [
'code' => 'hello-world'
]);
}
public function testRevokeAccessToken()
{
$provider = new TestApple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/../../resources/p256-private-key.p8',
'redirectUri' => 'none'
]);
$provider = m::mock($provider);
$client = m::mock(ClientInterface::class);
$client->shouldReceive('send')
->times(1)
->andReturn(new Response(200, [], json_encode([])));
$provider->setHttpClient($client);
$this->assertEmpty($provider->revokeAccessToken('hello-world', 'access_token'));
}
public function testRevokeAccessTokenFailedBecauseAppleHasError()
{
$this->expectException('Exception');
$this->expectExceptionMessage('invalid_request');
$provider = new TestApple([
'clientId' => 'mock.example',
'teamId' => 'mock.team.id',
'keyFileId' => 'mock.file.id',
'keyFilePath' => __DIR__ . '/../../resources/p256-private-key.p8',
'redirectUri' => 'none'
]);
$provider = m::mock($provider);
$client = m::mock(ClientInterface::class);
$client->shouldReceive('send')
->times(1)
->andReturn(new Response(400, [], json_encode(['error' => 'invalid_request'])));
$provider->setHttpClient($client);
$provider->revokeAccessToken('hello-world');
}
public function testFetchingOwnerDetails()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('fetchResourceOwnerDetails');
$method->setAccessible(true);
$arr = [
'name' => 'John Doe'
];
$_POST['user'] = json_encode($arr);
$data = $method->invokeArgs($provider, [new AccessToken(['access_token' => 'hello'])]);
$this->assertEquals($arr, $data);
}
/**
* @see https://github.com/patrickbussmann/oauth2-apple/issues/12
*/
public function testFetchingOwnerDetailsIssue12()
{
$class = new \ReflectionClass($this->provider);
$method = $class->getMethod('fetchResourceOwnerDetails');
$method->setAccessible(true);
public function testFetchingOwnerDetailsIssue12()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('fetchResourceOwnerDetails');
$method->setAccessible(true);
$_POST['user'] = '';
$data = $method->invokeArgs($this->provider, [new AccessToken(['access_token' => 'hello'])]);
$data = $method->invokeArgs($provider, [new AccessToken(['access_token' => 'hello'])]);
$this->assertEquals([], $data);
}
$this->assertEquals([], $data);
}
/**
* @expectedException Exception
*/
public function testNotImplementedGetResourceOwnerDetailsUrl()
{
$this->provider->getResourceOwnerDetailsUrl(new AccessToken(['access_token' => 'hello']));
}
public function testNotImplementedGetResourceOwnerDetailsUrl()
{
$this->expectException('Exception');
$provider = $this->getProvider();
$provider->getResourceOwnerDetailsUrl(new AccessToken(['access_token' => 'hello']));
}
/**
* @expectedException \League\OAuth2\Client\Provider\Exception\AppleAccessDeniedException
*/
public function testCheckResponse()
{
$class = new \ReflectionClass($this->provider);
$method = $class->getMethod('checkResponse');
$method->setAccessible(true);
public function testCheckResponse()
{
$this->expectException('\League\OAuth2\Client\Provider\Exception\AppleAccessDeniedException');
$this->expectExceptionMessage('invalid_client');
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('checkResponse');
$method->setAccessible(true);
$method->invokeArgs($this->provider, [new Response(400, []), [
'error' => 'invalid_client',
'code' => 400
]]);
}
$method->invokeArgs($provider, [new Response(400, []), [
'error' => 'invalid_client',
'code' => 400
]]);
}
public function testCreationOfResourceOwner()
{
$class = new \ReflectionClass($this->provider);
$method = $class->getMethod('createResourceOwner');
$method->setAccessible(true);
public function testResourceToArrayHasAttributes()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('createResourceOwner');
$method->setAccessible(true);
/** @var AppleResourceOwner $data */
$data = $method->invokeArgs($this->provider, [
[
'email' => 'john@doe.com',// <- Fake E-Mail from user input
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
]
],
new AccessToken([
'access_token' => 'hello',
'email' => 'john@doe.de',
'resource_owner_id' => '123.4.567'
])
]);
$this->assertEquals('john@doe.de', $data->getEmail());
$this->assertEquals('Doe', $data->getLastName());
$this->assertEquals('John', $data->getFirstName());
$this->assertEquals('123.4.567', $data->getId());
/** @var AppleResourceOwner $data */
$data = $method->invokeArgs($provider, [
[
'email' => 'john@doe.com',// <- Fake E-Mail from user input
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
]
],
new AccessToken([
'access_token' => 'hello',
'email' => 'john@doe.de',
'resource_owner_id' => '123.4.567'
])
]);
$expectedArray = [
'email' => 'john@doe.de',
'sub' => '123.4.567',
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
],
'isPrivateEmail' => null
];
$this->assertEquals($expectedArray, $data->toArray());
}
public function testCreationOfResourceOwnerWithName()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('createResourceOwner');
$method->setAccessible(true);
/** @var AppleResourceOwner $data */
$data = $method->invokeArgs($provider, [
[
'email' => 'john@doe.com',// <- Fake E-Mail from user input
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
]
],
new AccessToken([
'access_token' => 'hello',
'email' => 'john@doe.de',
'resource_owner_id' => '123.4.567'
])
]);
$this->assertEquals('john@doe.de', $data->getEmail());
$this->assertEquals('Doe', $data->getLastName());
$this->assertEquals('John', $data->getFirstName());
$this->assertEquals('123.4.567', $data->getId());
$this->assertFalse($data->isPrivateEmail());
$this->assertArrayHasKey('name', $data->toArray());
}
}
public function testCreationOfResourceOwnerWithoutName()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('createResourceOwner');
$method->setAccessible(true);
/** @var AppleResourceOwner $data */
$data = $method->invokeArgs($provider, [
[],
new AccessToken([
'access_token' => 'hello',
'email' => 'john@doe.de',
'resource_owner_id' => '123.4.567'
])
]);
$this->assertEquals('john@doe.de', $data->getEmail());
$this->assertNull($data->getLastName());
$this->assertNull($data->getFirstName());
}
}

View File

@@ -12,10 +12,10 @@ use League\OAuth2\Client\Provider\Apple;
class TestApple extends Apple
{
/**
* @return \Lcobucci\JWT\Signer\Key|null
* @inheritDoc
*/
public function getLocalKey()
public function getLocalKey(): string
{
return null;
return 'file://' . __DIR__ . '/../private_key.pem';
}
}

View File

@@ -2,40 +2,31 @@
namespace League\OAuth2\Client\Test\Token;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use Firebase\JWT\Key;
use League\OAuth2\Client\Token\AppleAccessToken;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;
use Mockery as m;
class AppleAccessTokenTest extends TestCase
{
public function tearDown()
{
m::close();
parent::tearDown();
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
#[PreserveGlobalState(false)]
#[RunInSeparateProcess]
public function testCreatingAccessToken()
{
$externalJWTMock = m::mock('overload:Firebase\JWT\JWT');
$externalJWTMock->shouldReceive('decode')
->with('something', 'examplekey', ['RS256'])
->with('something', 'examplekey')
->once()
->andReturn([
'sub' => '123.abc.123'
'sub' => '123.abc.123',
'email_verified' => true,
'email' => 'john@doe.com',
'is_private_email' => true
]);
$externalJWKMock = m::mock('overload:Firebase\JWT\JWK');
$externalJWKMock->shouldReceive('parseKeySet')
->once()
->andReturn(['examplekey']);
$accessToken = new AppleAccessToken($this->getClient(1), [
$accessToken = new AppleAccessToken(['examplekey'], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600,
@@ -45,11 +36,28 @@ class AppleAccessTokenTest extends TestCase
$this->assertEquals('something', $accessToken->getIdToken());
$this->assertEquals('123.abc.123', $accessToken->getResourceOwnerId());
$this->assertEquals('access_token', $accessToken->getToken());
$this->assertEquals('john@doe.com', $accessToken->getEmail());
$this->assertTrue($accessToken->isPrivateEmail());
$this->assertTrue(true);
}
public function testCreateFailsBecauseNoIdTokenIsSet()
{
$this->expectException('\InvalidArgumentException');
$this->expectExceptionMessage('Required option not passed: "id_token"');
new AppleAccessToken(['examplekey'], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'abc.0.def'
]);
}
public function testCreatingRefreshToken()
{
$refreshToken = new AppleAccessToken($this->getClient(0), [
$refreshToken = new AppleAccessToken([], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600
@@ -57,17 +65,25 @@ class AppleAccessTokenTest extends TestCase
$this->assertEquals('access_token', $refreshToken->getToken());
}
private function getClient($times)
#[PreserveGlobalState(false)]
#[RunInSeparateProcess]
public function testCreatingAccessTokenFailsBecauseNoDecodingIsPossible()
{
$client = m::mock('GuzzleHttp\ClientInterface');
if ($times > 0) {
$client->shouldReceive('request')
->times($times)
->withArgs(['GET', 'https://appleid.apple.com/auth/keys'])
->andReturn(new Response())
;
}
$this->expectException('\Exception');
$this->expectExceptionMessage('Got no data within "id_token"!');
return $client;
$externalJWTMock = m::mock('overload:Firebase\JWT\JWT');
$externalJWTMock->shouldReceive('decode')
->with('something', 'examplekey')
->once()
->andReturnNull();
new AppleAccessToken(['examplekey'], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'abc.0.def',
'id_token' => 'something'
]);
}
}

16
test/src/private_key.pem Normal file
View File

@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJ/7C8IWBvfYj01Q
+4mldM50B8qPVHIoDEyxNsNXLcYqe3GkkHklKFRKidaxOxx6MBAQz9l2bgfh6Stt
NWkeJ3I8oZ73zju/twjqhQhzveST+xwpUP935xgkbYmZVEM9JRj6wQ52pYA2sdkx
/5wqvF7oJ7dlsGIga6PbV0/i59kdAgMBAAECgYBuCUmkHGx8irq+LkZk/aXi3tIB
FCa8Qil7kqSdJVh5pfy0RMGOYe1kVMSMI+kJhE2Mr1OXOqshxtQPJ5WGENSF2yBq
BRbFn/Xeweh1MpXT7no1dhaS0Rfn01ScvgDMc6NkuiTwshSxOhnuaFVVxYPJKFP4
aqY6Dp8vSwrISlnPbQJBAMxHtPmIU9uuDq7fYAl+5WK2S4gY5IPl2y0xJIwoN6+q
hc+3ZkMIDrcwzzqxcV82jBU7CnnV/CmZ+r99e4jmbAMCQQDIfBicV7YuuBLXlDGe
LSCSqP1+V9o2ZVtR0PM4Z4346xJN/rJ4LWz2KexY//nKUMsVIyJuYp1pJpN21K6p
8exfAkEAia9bH0TvoIV0iBEunbfVy+6qghSlEPGABLm2tHD294OrpREr78oigP54
7kpi65XMXRLqQKwlxbRu+VoORXtpGQJABxTzHZqvkcjoyXqvogHAE84qXismRyOf
bS1vWf+2cSOEmwKzNTGNlsh2U9J+9VmTQuTh03piSxOUw+7RWKl2CwJAUdoH4is3
qRdiRtRcox8QmIgtFFN5r7ZcFfJO4wjrjM5KbB3mdYoxu5jE8bkR//t5Unz5mqqu
1uo+Dq/SJyXi7w==
-----END PRIVATE KEY-----