mirror of
https://github.com/code-rhapsodie/oauth2-apple.git
synced 2026-03-24 21:02:08 +01:00
Compare commits
51 Commits
0.2.2
...
update-cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
344c96aeaf | ||
|
|
db5789d4d2 | ||
|
|
f4ee42b17c | ||
|
|
6d17ab2e2d | ||
|
|
31ece38879 | ||
|
|
7a03f29cc5 | ||
|
|
23a643494f | ||
|
|
e645a8afa2 | ||
|
|
fdcae826ed | ||
|
|
50fe6079e3 | ||
|
|
4b913d776d | ||
|
|
6952cbec6d | ||
|
|
ba795803bb | ||
|
|
a72cfeba83 | ||
|
|
831c8e0e25 | ||
|
|
942a875dc6 | ||
|
|
e6626c4575 | ||
|
|
3f0f659741 | ||
|
|
badae8f295 | ||
|
|
5d3bd66b5b | ||
|
|
6b38a21212 | ||
|
|
d5048c7f76 | ||
|
|
74818d3854 | ||
|
|
561ae0f92c | ||
|
|
cb1bf60335 | ||
|
|
16c3708cf8 | ||
|
|
d9d17976f1 | ||
|
|
3a4576b801 | ||
|
|
51c96fba61 | ||
|
|
a317e52085 | ||
|
|
415e29753b | ||
|
|
61e5f98312 | ||
|
|
5f20997c99 | ||
|
|
f6fe8c0b1b | ||
|
|
113f72fa62 | ||
|
|
eb4d697dda | ||
|
|
95cc38cd0c | ||
|
|
a08ded7b3a | ||
|
|
31c61d334b | ||
|
|
d649df8313 | ||
|
|
6702931e2e | ||
|
|
5598a94a36 | ||
|
|
e191fd0988 | ||
|
|
8c49a59451 | ||
|
|
45903a3f90 | ||
|
|
06ef0a50d1 | ||
|
|
a6b002a062 | ||
|
|
1b47b8e9d3 | ||
|
|
3f23b1d156 | ||
|
|
81164c4b8a | ||
|
|
a45b7cdc9b |
78
.github/workflows/continuous-integration.yml
vendored
Normal file
78
.github/workflows/continuous-integration.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
composer.phar
|
||||
composer.lock
|
||||
.DS_Store
|
||||
.phpunit.result.cache
|
||||
|
||||
51
.travis.yml
51
.travis.yml
@@ -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
|
||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
55
README.md
55
README.md
@@ -1,13 +1,12 @@
|
||||
# Sign in with Apple ID Provider for OAuth 2.0 Client
|
||||
[](https://github.com/patrickbussmann/oauth2-apple/releases)
|
||||
[](LICENSE.md)
|
||||
[](https://travis-ci.org/patrickbussmann/oauth2-apple)
|
||||
[](https://scrutinizer-ci.com/g/patrickbussmann/oauth2-apple/code-structure)
|
||||
[](https://scrutinizer-ci.com/g/patrickbussmann/oauth2-apple)
|
||||
[](https://packagist.org/packages/patrickbussmann/oauth2-apple)
|
||||
[](https://github.com/code-rhapsodie/oauth2-apple/releases)
|
||||
[](LICENSE)
|
||||
[](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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
phpunit.xml
36
phpunit.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\OAuth2\Client\Provider\Exception;
|
||||
|
||||
class AppleAccessDeniedException extends IdentityProviderException
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
16
test/src/private_key.pem
Normal 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-----
|
||||
Reference in New Issue
Block a user