-
Notifications
You must be signed in to change notification settings - Fork 452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Apple - new option for using private key instead secret key. #1019
base: master
Are you sure you want to change the base?
Changes from all commits
5641e50
37df282
d9048ea
cab4fb5
e00146f
a8d76d3
372d8d1
806916a
0618773
c3a25b5
add78fd
f9bc3fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
namespace SocialiteProviders\Apple; | ||
|
||
use Carbon\CarbonImmutable; | ||
use Lcobucci\JWT\Configuration; | ||
|
||
class AppleToken | ||
{ | ||
private Configuration $jwtConfig; | ||
|
||
public function __construct(Configuration $jwtConfig) | ||
{ | ||
$this->jwtConfig = $jwtConfig; | ||
} | ||
|
||
public function generate(): string | ||
{ | ||
$now = CarbonImmutable::now(); | ||
|
||
$token = $this->jwtConfig->builder() | ||
->issuedBy(config('services.apple.team_id')) | ||
->issuedAt($now) | ||
->expiresAt($now->addHour()) | ||
->permittedFor(Provider::URL) | ||
->relatedTo(config('services.apple.client_id')) | ||
->withHeader('kid', config('services.apple.key_id')) | ||
->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey()); | ||
|
||
return $token->toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ | |||||
use Firebase\JWT\JWK; | ||||||
use GuzzleHttp\Client; | ||||||
use GuzzleHttp\RequestOptions; | ||||||
use Illuminate\Http\Request; | ||||||
use Illuminate\Support\Arr; | ||||||
use Illuminate\Support\Facades\Cache; | ||||||
use Illuminate\Support\Str; | ||||||
|
@@ -24,7 +25,7 @@ class Provider extends AbstractProvider | |||||
{ | ||||||
public const IDENTIFIER = 'APPLE'; | ||||||
|
||||||
private const URL = 'https://appleid.apple.com'; | ||||||
public const URL = 'https://appleid.apple.com'; | ||||||
|
||||||
protected $scopes = [ | ||||||
'name', | ||||||
|
@@ -38,6 +39,23 @@ class Provider extends AbstractProvider | |||||
|
||||||
protected $scopeSeparator = ' '; | ||||||
|
||||||
/** | ||||||
* JWT Configuration. | ||||||
* | ||||||
* @var ?Configuration | ||||||
*/ | ||||||
protected $jwtConfig = null; | ||||||
|
||||||
/** | ||||||
* Private Key. | ||||||
* | ||||||
* @var string | ||||||
*/ | ||||||
protected $privateKey = ''; | ||||||
|
||||||
/** | ||||||
* {@inheritdoc} | ||||||
*/ | ||||||
protected function getAuthUrl($state): string | ||||||
{ | ||||||
return $this->buildAuthUrlFromBase(self::URL.'/auth/authorize', $state); | ||||||
|
@@ -75,7 +93,7 @@ protected function getCodeFields($state = null) | |||||
public function getAccessTokenResponse($code) | ||||||
{ | ||||||
$response = $this->getHttpClient()->post($this->getTokenUrl(), [ | ||||||
RequestOptions::HEADERS => ['Authorization' => 'Basic '.base64_encode($this->clientId.':'.$this->clientSecret)], | ||||||
RequestOptions::HEADERS => ['Authorization' => 'Basic '.base64_encode($this->clientId.':'.$this->getClientSecret())], | ||||||
RequestOptions::FORM_PARAMS => $this->getTokenFields($code), | ||||||
]); | ||||||
|
||||||
|
@@ -87,12 +105,53 @@ public function getAccessTokenResponse($code) | |||||
*/ | ||||||
protected function getUserByToken($token) | ||||||
{ | ||||||
static::verify($token); | ||||||
$this->checkToken($token); | ||||||
$claims = explode('.', $token)[1]; | ||||||
|
||||||
return json_decode(base64_decode($claims), true); | ||||||
} | ||||||
|
||||||
protected function getClientSecret() | ||||||
{ | ||||||
if (!$this->jwtConfig) { | ||||||
$this->getJwtConfig(); // Generate Client Secret from private key if not set. | ||||||
} | ||||||
|
||||||
return $this->clientSecret; | ||||||
} | ||||||
|
||||||
protected function getJwtConfig() | ||||||
{ | ||||||
if (!$this->jwtConfig) { | ||||||
$private_key_path = $this->getConfig('private_key', ''); | ||||||
$private_key_passphrase = $this->getConfig('passphrase', ''); | ||||||
$signer = $this->getConfig('signer', ''); | ||||||
|
||||||
if (empty($signer) || !class_exists($signer)) { | ||||||
$signer = !empty($private_key_path) ? \Lcobucci\JWT\Signer\Ecdsa\Sha256::class : AppleSignerNone::class; | ||||||
} | ||||||
|
||||||
if (!empty($private_key_path) && file_exists($private_key_path)) { | ||||||
$this->privateKey = file_get_contents($private_key_path); | ||||||
} else { | ||||||
$this->privateKey = $private_key_path; // Support for plain text private keys | ||||||
} | ||||||
|
||||||
$this->jwtConfig = Configuration::forSymmetricSigner( | ||||||
new $signer(), | ||||||
AppleSignerInMemory::plainText($this->privateKey, $private_key_passphrase) | ||||||
); | ||||||
|
||||||
if (!empty($this->privateKey)) { | ||||||
$appleToken = new AppleToken($this->getJwtConfig()); | ||||||
$this->clientSecret = $appleToken->generate(); | ||||||
config()->set('services.apple.client_secret', $this->clientSecret); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might need to set this to a variable on the class with above switch to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you think so? |
||||||
} | ||||||
} | ||||||
|
||||||
return $this->jwtConfig; | ||||||
} | ||||||
|
||||||
/** | ||||||
* Return the user given the identity token provided on the client | ||||||
* side by Apple. | ||||||
|
@@ -110,20 +169,16 @@ public function userByIdentityToken(string $token): User | |||||
} | ||||||
|
||||||
/** | ||||||
* Verify Apple jwt. | ||||||
* Verify Apple JWT. | ||||||
* | ||||||
* @param string $jwt | ||||||
* @return bool | ||||||
* | ||||||
* @see https://appleid.apple.com/auth/keys | ||||||
*/ | ||||||
public static function verify($jwt) | ||||||
public function checkToken($jwt) | ||||||
{ | ||||||
$jwtContainer = Configuration::forSymmetricSigner( | ||||||
new AppleSignerNone, | ||||||
AppleSignerInMemory::plainText('') | ||||||
); | ||||||
$token = $jwtContainer->parser()->parse($jwt); | ||||||
$token = $this->getJwtConfig()->parser()->parse($jwt); | ||||||
|
||||||
$data = Cache::remember('socialite:Apple-JWKSet', 5 * 60, function () { | ||||||
$response = (new Client)->get(self::URL.'/auth/keys'); | ||||||
|
@@ -143,7 +198,7 @@ public static function verify($jwt) | |||||
]; | ||||||
|
||||||
try { | ||||||
$jwtContainer->validator()->assert($token, ...$constraints); | ||||||
$this->jwtConfig->validator()->assert($token, ...$constraints); | ||||||
|
||||||
return true; | ||||||
} catch (RequiredConstraintsViolated $e) { | ||||||
|
@@ -154,6 +209,25 @@ public static function verify($jwt) | |||||
throw new InvalidStateException('Invalid JWT Signature'); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Verify Apple jwt via static function. | ||||||
* | ||||||
* @param string $jwt | ||||||
* | ||||||
* @return bool | ||||||
* | ||||||
* @see https://appleid.apple.com/auth/keys | ||||||
*/ | ||||||
public static function verify($jwt) | ||||||
{ | ||||||
return (new self( | ||||||
new Request(), | ||||||
config('services.apple.client_id'), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
And other calls to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can call $this-> in static function, or ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh my bad, didn't realise it was static. Is that why it overrides the global config above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes |
||||||
config('services.apple.client_secret'), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this method still work if the secret is dervied? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it will work. I had to do that (create new object) because this function is called statically (don't know why) and it was necessary to call other non static functions. |
||||||
config('services.apple.redirect') | ||||||
))->checkToken($jwt); | ||||||
} | ||||||
|
||||||
/** | ||||||
* {@inheritdoc} | ||||||
*/ | ||||||
|
@@ -251,9 +325,9 @@ protected function getRevokeUrl(): string | |||||
public function revokeToken(string $token, string $hint = 'access_token') | ||||||
{ | ||||||
return $this->getHttpClient()->post($this->getRevokeUrl(), [ | ||||||
RequestOptions::FORM_PARAMS => [ | ||||||
RequestOptions::FORM_PARAMS => [ | ||||||
'client_id' => $this->clientId, | ||||||
'client_secret' => $this->clientSecret, | ||||||
'client_secret' => $this->getClientSecret(), | ||||||
'token' => $token, | ||||||
'token_type_hint' => $hint, | ||||||
], | ||||||
|
@@ -283,4 +357,12 @@ public function refreshToken($refreshToken): ResponseInterface | |||||
], | ||||||
]); | ||||||
} | ||||||
|
||||||
/** | ||||||
* {@inheritdoc} | ||||||
*/ | ||||||
public static function additionalConfigKeys() | ||||||
{ | ||||||
return ['private_key', 'passphrase', 'signer']; | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, there is no bc break here, existing integrations using the secret key will continiue to work fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, if you pass the path to the private key file, it will get the contents of the private key file, otherwise you put the private key contents by default.