31 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
16 changed files with 441 additions and 359 deletions

View File

@@ -1,80 +1,78 @@
name: 'CI'
name: CI
on:
pull_request:
push:
branches:
- 'main'
- main
env:
COMPOSER_ROOT_VERSION: '1.99.99'
jobs:
lint:
name: 'Lint'
runs-on: 'ubuntu-latest'
name: Lint
runs-on: ubuntu-latest
steps:
- uses: 'actions/checkout@v2'
- uses: 'shivammathur/setup-php@v2'
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: 'none'
ini-values: 'memory_limit=-1'
tools: 'composer:v2'
- uses: 'ramsey/composer-install@v1'
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'
run: ./vendor/bin/parallel-lint src test
coding-standards:
name: 'Coding Standards'
runs-on: 'ubuntu-latest'
name: Coding Standards
runs-on: ubuntu-latest
steps:
- uses: 'actions/checkout@v2'
- uses: 'shivammathur/setup-php@v2'
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: 'none'
ini-values: 'memory_limit=-1'
tools: 'composer:v2'
- uses: 'ramsey/composer-install@v1'
- name: 'Check coding standards'
run: './vendor/bin/phpcs src --standard=psr2 -sp --colors'
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'
name: Unit Tests
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
php-version:
- '5.6'
- '7.0'
- '7.1'
- '7.2'
- '7.3'
- '7.4'
- '8.0'
- '8.1'
- '8.2'
- '8.4'
- '8.5'
dependencies:
- lowest
- highest
experimental:
- false
include:
- php-version: '8.1'
experimental: true
composer-options: '--ignore-platform-reqs'
- php-version: '8.3'
experimental: false
steps:
- uses: 'actions/checkout@v2'
- uses: 'shivammathur/setup-php@v2'
- 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@v1'
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 --colors=always --coverage-clover build/logs/clover.xml'
- name: 'Publish coverage report to Codecov'
uses: 'codecov/codecov-action@v1'
- 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,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,44 @@ 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

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,14 +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/main.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)
[![codecov](https://codecov.io/gh/patrickbussmann/oauth2-apple/branch/main/graph/badge.svg?token=TN3ZNVHUXV)](https://codecov.io/gh/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:
@@ -23,7 +21,7 @@ Maybe Apple changes this sometime.
To install, use composer:
```
composer require patrickbussmann/oauth2-apple
composer require code-rhapsodie/oauth2-apple
```
## Usage
@@ -88,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.
@@ -130,15 +156,15 @@ $ ./vendor/bin/phpunit
## Contributing
Please see [CONTRIBUTING](https://github.com/patrickbussmann/oauth2-apple/blob/main/CONTRIBUTING.md) for details.
Please see [CONTRIBUTING](https://github.com/code-rhapsodie/oauth2-apple/blob/main/CONTRIBUTING.md) for details.
## Credits
- [All Contributors](https://github.com/patrickbussmann/oauth2-apple/contributors)
- [All Contributors](https://github.com/code-rhapsodie/oauth2-apple/contributors)
Template for this repository was the [LinkedIn](https://github.com/thephpleague/oauth2-linkedin).
## License
The MIT License (MIT). Please see [License File](https://github.com/patrickbussmann/oauth2-apple/blob/main/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,29 +0,0 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default:
target: auto
threshold: 0%
patch:
default:
target: auto
threshold: 0%
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "reach,diff,flags,tree"
behavior: default
require_changes: false

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,17 +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.4 || ^4.0"
"league/oauth2-client": "^2.0",
"firebase/php-jwt": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
"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": "^2.3 || ^3.0",
"ext-json": "*"
"squizlabs/php_codesniffer": "^3.0 || ^4.0",
"composer/semver": "^3.0"
},
"autoload": {
"psr-4": {
@@ -43,7 +46,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "0.3.x-dev"
"dev-master": "0.4.x-dev"
}
}
}

View File

@@ -1,22 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">./</directory>
</include>
<exclude>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./test</directory>
</exclude>
<report>
<clover outputFile="./build/coverage/log/coverage.xml"/>
<html outputDirectory="./build/coverage/html" lowUpperBound="35" highLowerBound="70"/>
</report>
</coverage>
<logging/>
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./test/</directory>
</testsuite>
</testsuites>
<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>
</phpunit>

View File

@@ -1,20 +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\Configuration;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer;
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
@@ -24,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 = [])
{
@@ -67,64 +65,49 @@ class Apple extends AbstractProvider
}
/**
* Creates an access token from a response.
*
* The grant that was used to fetch the response can be used to provide
* additional context.
*
* @param array $response
* @param AbstractGrant $grant
* @return AccessTokenInterface
* @inheritDoc
*/
protected function createAccessToken(array $response, AbstractGrant $grant)
protected function createAccessToken(array $response, AbstractGrant $grant): AccessTokenInterface
{
return new AppleAccessToken($this->getAppleKeys(), $response);
}
/**
* @return string[] Apple's JSON Web Keys
* @return array<string, Key> Apple's JSON Web Keys
*/
private function getAppleKeys()
private function getAppleKeys(): array
{
$response = $this->httpClient->request('GET', 'https://appleid.apple.com/auth/keys');
if ($response && $response->getStatusCode() === 200) {
return JWK::parseKeySet(json_decode($response->getBody()->getContents(), true));
if ($response->getStatusCode() === 200) {
return JWK::parseKeySet(json_decode($response->getBody()->__toString(), true));
}
return [];
}
/**
* Get the string used to separate scopes.
*
* @return string
* @inheritDoc
*/
protected function getScopeSeparator()
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)
{
@@ -133,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;
}
@@ -181,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
);
@@ -199,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
]
),
@@ -222,54 +202,92 @@ class Apple extends AbstractProvider
/**
* {@inheritDoc}
*/
public function getAccessToken($grant, array $options = [])
public function getAccessToken($grant, array $options = []): AccessTokenInterface
{
$configuration = $this->getConfiguration();
$time = new \DateTimeImmutable();
$time = $time->setTime($time->format('H'), $time->format('i'), $time->format('s'));
$expiresAt = $time->modify('+1 Hour');
$expiresAt = $expiresAt->setTime($expiresAt->format('H'), $expiresAt->format('i'), $expiresAt->format('s'));
$time = time();
$token = $configuration->builder()
->issuedBy($this->teamId)
->permittedFor('https://appleid.apple.com')
->issuedAt($time)
->expiresAt($expiresAt)
->relatedTo($this->clientId)
->withHeader('alg', 'ES256')
->withHeader('kid', $this->keyFileId)
->getToken($configuration->signer(), $configuration->signingKey());
$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' => $token->toString()
'client_secret' => $jwt
];
return parent::getAccessToken($grant, $options);
}
/**
* @return Configuration
* Revokes an access or refresh token using a specified token.
*/
public function getConfiguration()
public function revokeAccessToken(string $token, ?string $tokenTypeHint = null)
{
if (method_exists(Signer\Ecdsa\Sha256::class, 'create')) {
return Configuration::forSymmetricSigner(
Signer\Ecdsa\Sha256::create(),
$this->getLocalKey()
);
} else {
return Configuration::forSymmetricSigner(
new Signer\Ecdsa\Sha256(),
$this->getLocalKey()
);
$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);
}
/**
* @return Key
*/
public function getLocalKey()
public function getLocalKey(): string
{
return LocalFileReference::file($this->keyFilePath);
if (!file_exists($this->keyFilePath)) {
throw new \InvalidArgumentException('Could not read key file');
}
return file_get_contents($this->keyFilePath);
}
}

View File

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

View File

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

View File

@@ -2,32 +2,20 @@
namespace League\OAuth2\Client\Token;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use GuzzleHttp\ClientInterface;
use Firebase\JWT\Key;
use InvalidArgumentException;
class AppleAccessToken extends AccessToken
{
/**
* @var string
*/
protected $idToken;
protected string $idToken;
protected string $email;
protected ?bool $isPrivateEmail = null;
/**
* @var string
*/
protected $email;
/**
* @var boolean
*/
protected $isPrivateEmail;
/**
* Constructs an access token.
*
* @param string[] $keys Valid Apple JWT keys
* @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`.
@@ -45,7 +33,12 @@ class AppleAccessToken extends AccessToken
$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) {
@@ -53,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'];
@@ -72,34 +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 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,17 +2,15 @@
namespace League\OAuth2\Client\Test\Provider;
use Firebase\JWT\JWT;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key;
use League\OAuth2\Client\Provider\Apple;
use League\OAuth2\Client\Provider\AppleResourceOwner;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Token\AppleAccessToken;
use League\OAuth2\Client\Tool\QueryBuilderTrait;
use PHPUnit\Framework\TestCase;
use Mockery as m;
use PHPUnit\Framework\TestCase;
class AppleTest extends TestCase
{
@@ -130,20 +128,27 @@ class AppleTest extends TestCase
]);
$provider = m::mock($provider);
$time = time();
$configuration = Configuration::forUnsecuredSigner();
$payload = [
'iss' => 'test-team-id',
'iat' => $time,
'exp' => $time + 3600,
'aud' => 'https://appleid.apple.com',
'sub' => 'test-client',
];
$jwt = JWT::encode(
$payload,
'file://' . __DIR__ . '/../private_key.pem',
'ES256',
'test',
[
'kid' => 'test',
'alg' => 'ES256',
]
);
$time = new \DateTimeImmutable();
$expiresAt = $time->modify('+1 Hour');
$token = $configuration->builder()
->issuedBy('test-team-id')
->permittedFor('https://appleid.apple.com')
->issuedAt($time)
->expiresAt($expiresAt)
->relatedTo('test-client')
->withHeader('alg', 'RS256')
->withHeader('kid', 'test')
->getToken($configuration->signer(), $configuration->signingKey());
$client = m::mock(ClientInterface::class);
$client->shouldReceive('request')
@@ -156,7 +161,7 @@ class AppleTest extends TestCase
'token_type' => 'Bearer',
'expires_in' => 3600,
'refresh_token' => 'r4a6e8b9c50104b78bc86b0d2649353fa.0.mrwxq.54joUj40j0cpuMANRtRjfg',
'id_token' => $token->toString()
'id_token' => $jwt
])));
$provider->setHttpClient($client);
@@ -199,6 +204,49 @@ class AppleTest extends TestCase
]);
}
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();
@@ -253,6 +301,40 @@ class AppleTest extends TestCase
]]);
}
public function testResourceToArrayHasAttributes()
{
$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'
])
]);
$expectedArray = [
'email' => 'john@doe.de',
'sub' => '123.4.567',
'name' => [
'firstName' => 'John',
'lastName' => 'Doe'
],
'isPrivateEmail' => null
];
$this->assertEquals($expectedArray, $data->toArray());
}
public function testCreationOfResourceOwnerWithName()
{
$provider = $this->getProvider();
@@ -303,12 +385,4 @@ class AppleTest extends TestCase
$this->assertNull($data->getLastName());
$this->assertNull($data->getFirstName());
}
public function testGetConfiguration()
{
$provider = m::mock(Apple::class)->makePartial();
$provider->shouldReceive('getLocalKey')->andReturn(m::mock(Key::class));
$this->assertInstanceOf(Configuration::class, $provider->getConfiguration());
}
}

View File

@@ -2,7 +2,6 @@
namespace League\OAuth2\Client\Test\Provider;
use Lcobucci\JWT\Configuration;
use League\OAuth2\Client\Provider\Apple;
/**
@@ -13,18 +12,10 @@ use League\OAuth2\Client\Provider\Apple;
class TestApple extends Apple
{
/**
* {@inheritDoc}
* @inheritDoc
*/
public function getConfiguration()
public function getLocalKey(): string
{
return Configuration::forUnsecuredSigner();
}
/**
* {@inheritDoc}
*/
public function getLocalKey()
{
return null;
return 'file://' . __DIR__ . '/../private_key.pem';
}
}

View File

@@ -2,21 +2,22 @@
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
{
/**
* @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',
@@ -37,6 +38,8 @@ class AppleAccessTokenTest extends TestCase
$this->assertEquals('access_token', $accessToken->getToken());
$this->assertEquals('john@doe.com', $accessToken->getEmail());
$this->assertTrue($accessToken->isPrivateEmail());
$this->assertTrue(true);
}
public function testCreateFailsBecauseNoIdTokenIsSet()
@@ -62,10 +65,8 @@ class AppleAccessTokenTest extends TestCase
$this->assertEquals('access_token', $refreshToken->getToken());
}
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
#[PreserveGlobalState(false)]
#[RunInSeparateProcess]
public function testCreatingAccessTokenFailsBecauseNoDecodingIsPossible()
{
$this->expectException('\Exception');
@@ -73,7 +74,7 @@ class AppleAccessTokenTest extends TestCase
$externalJWTMock = m::mock('overload:Firebase\JWT\JWT');
$externalJWTMock->shouldReceive('decode')
->with('something', 'examplekey', ['RS256'])
->with('something', 'examplekey')
->once()
->andReturnNull();

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-----