51 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
Patrick Bußmann
5598a94a36 Added leeway usage to README 2021-01-05 19:33:41 +01:00
Patrick Bußmann
e191fd0988 Merge remote-tracking branch 'jmalinens/use_guzzle'
# Conflicts:
#	src/Token/AppleAccessToken.php
2021-01-05 12:53:11 +01:00
Patrick Bußmann
8c49a59451 Merge remote-tracking branch 'NgSekLong/patch-1' 2021-01-05 08:53:45 +01:00
Patrick Bußmann
45903a3f90 Merge remote-tracking branch 'giganti-consultoria/master' 2021-01-05 08:53:39 +01:00
zhangv
06ef0a50d1 support Lcobucci-jwt v3.4+
and compatible to the old versions
2020-12-11 14:45:06 +08:00
Juris Malinens
a6b002a062 use guzzle instance from AbstractProvider to get apple auth keys instead of file_get_contents 2020-11-27 17:05:39 +02:00
NgSekLong
1b47b8e9d3 Update README include no scope instruction
Add mention for how to disable scope "Don't need email and name", and how to modify the sample code to get back `code`

i.e. Change all `$_POST` to `$_GET`
2020-09-10 11:07:06 +08:00
William Martins
3f23b1d156 Code Formatter 2020-07-31 16:50:28 -03:00
William Martins
81164c4b8a Use guzzle http client instead of file_get_contents
Many servers have allow_fopen_url disabled so they cant use file_get_contents to load apple keys.
2020-07-31 16:38:39 -03:00
Bogdan Dovgopol
a45b7cdc9b Check if name scope is set before trying to return firstName or lastName 2020-07-23 12:06:08 +10:00
16 changed files with 775 additions and 473 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,51 +0,0 @@
language: php
matrix:
include:
- php: 5.6
- php: 7.0
- php: 7.1
- php: 7.2
- php: 7.3
- 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
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,78 @@ 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
- Using guzzle http instead of file_get_contents [#14](https://github.com/patrickbussmann/oauth2-apple/pull/14)/[#17](https://github.com/patrickbussmann/oauth2-apple/pull/17) (thanks to [jmalinens](https://github.com/jmalinens) and [williamxsp](https://github.com/williamxsp))
- README no scope instruction [#15](https://github.com/patrickbussmann/oauth2-apple/pull/15) (thanks to [NgSekLong](https://github.com/NgSekLong))
- README leeway usage [#18](https://github.com/patrickbussmann/oauth2-apple/issues/18) (thanks to [lukequinnell](https://github.com/lukequinnell))
### Fixed
- Fixed getting first and last name issues [#13](https://github.com/patrickbussmann/oauth2-apple/pull/13) (thanks to [bogdandovgopol](https://github.com/bogdandovgopol))
## 0.2.1 - 2020-02-13
### 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
@@ -32,11 +31,14 @@ Usage is the same as The League's OAuth client, using `\League\OAuth2\Client\Pro
### Authorization 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
'keyFilePath' => '{apple-key-file-path}', // __DIR__ . '/AuthKey_1ABC6523AA.p8' -> Download key above
'redirectUri' => 'https://example.com/callback-url',
]);
@@ -84,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.
@@ -107,6 +137,7 @@ At the time of authoring this documentation, the following scopes are available.
Please note that you will get this informations only at the first log in of the user!
In the following log ins you'll get only the user id!
If you only want to get the user id, you can set the `scope` as ` `, then change all the `$_POST` to `$_GET`.
### Refresh Tokens
@@ -125,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).
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"
"league/oauth2-client": "^2.0",
"firebase/php-jwt": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"mockery/mockery": "~0.9",
"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,37 +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"
syntaxCheck="false"
>
<logging>
<log type="coverage-html"
target="./build/coverage/html"
charset="UTF-8"
highlight="false"
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($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 = time();
$token = (new Builder())
->issuedBy($this->teamId)
->permittedFor('https://appleid.apple.com')
->issuedAt($time)
->expiresAt($time + 600)
->relatedTo($this->clientId)
->withClaim('sub', $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,98 +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);
}
public function getFirstName(): ?string
{
$name = $this->getAttribute('name');
if (is_array($name)) {
return $name['firstName'];
}
return null;
}
/**
* Get user first name
*
* @return string|null
*/
public function getFirstName()
{
return $this->getAttribute('name')['firstName'];
}
/**
* 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
{
return $this->getAttribute('name')['lastName'];
$name = $this->getAttribute('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,37 +2,27 @@
namespace League\OAuth2\Client\Token;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
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;
/**
* Constructs an access token.
*
* @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(array $options = [])
public function __construct(array $keys, array $options = [])
{
if (array_key_exists('refresh_token', $options)) {
if (empty($options['id_token'])) {
@@ -40,11 +30,15 @@ class AppleAccessToken extends AccessToken
}
$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) {
@@ -52,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'];
@@ -71,42 +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()
{
return JWK::parseKeySet(json_decode(file_get_contents('https://appleid.apple.com/auth/keys'), true));
}
/**
* @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,28 +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 Mockery as m;
use PHPUnit\Framework\TestCase;
class AppleTest extends \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',
@@ -32,62 +30,49 @@ class AppleTest extends \PHPUnit_Framework_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);
@@ -97,22 +82,24 @@ class AppleTest extends \PHPUnit_Framework_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']);
@@ -120,135 +107,282 @@ class AppleTest extends \PHPUnit_Framework_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 = time();
$token = (new Builder())
->issuedBy('test-team-id')
->permittedFor('https://appleid.apple.com')
->issuedAt($time)
->expiresAt($time + 600)
->relatedTo('test-client')
->withClaim('sub', 'test')
->withHeader('alg', 'RS256')
->withHeader('kid', 'test')
->getToken();
$time = time();
$client = m::mock('GuzzleHttp\ClientInterface');
$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']));
}
public function testCheckResponse()
{
$this->setExpectedException(AppleAccessDeniedException::class, 'invalid_client', 400);
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);
$class = new \ReflectionClass($this->provider);
$method = $class->getMethod('checkResponse');
$method->setAccessible(true);
$method->invokeArgs($provider, [new Response(400, []), [
'error' => 'invalid_client',
'code' => 400
]]);
}
$method->invokeArgs($this->provider, [new Response(400, []), [
'error' => 'invalid_client',
'code' => 400
]]);
}
public function testResourceToArrayHasAttributes()
{
$provider = $this->getProvider();
$class = new \ReflectionClass($provider);
$method = $class->getMethod('createResourceOwner');
$method->setAccessible(true);
public function testCreationOfResourceOwner()
{
$class = new \ReflectionClass($this->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'
])
]);
$expectedArray = [
'email' => 'john@doe.de',
'sub' => '123.4.567',
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
],
'isPrivateEmail' => null
];
$this->assertEquals($expectedArray, $data->toArray());
}
/** @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());
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,38 +2,31 @@
namespace League\OAuth2\Client\Test\Token;
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([
$accessToken = new AppleAccessToken(['examplekey'], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600,
@@ -43,15 +36,54 @@ 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([
$refreshToken = new AppleAccessToken([], [
'access_token' => 'access_token',
'token_type' => 'Bearer',
'expires_in' => 3600
]);
$this->assertEquals('access_token', $refreshToken->getToken());
}
#[PreserveGlobalState(false)]
#[RunInSeparateProcess]
public function testCreatingAccessTokenFailsBecauseNoDecodingIsPossible()
{
$this->expectException('\Exception');
$this->expectExceptionMessage('Got no data within "id_token"!');
$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-----