Skip to content
Snippets Groups Projects
Unverified Commit 12dea69d authored by Pavel Vyskočil's avatar Pavel Vyskočil
Browse files

Add support for Sign in with Apple

parent 001d0f9d
No related branches found
No related tags found
No related merge requests found
......@@ -276,6 +276,22 @@ View [full Google](/docs/GOOGLE.md) instructions.
),
```
### Generic Apple
View [full Apple](/docs/APPLE.md) instructions.
```php
'apple' => array(
'authoauth2:AppleAuth',
'clientId' => 'CLIENT_ID',
'clientSecret' => 'CLIENT_SECRET',
'redirectUri' => 'REDIRECT_URI',
//scopes: Only email is available
),
```
# Debugging
## HTTP Logging
......
<?php
$attributemap = array(
'apple.sub' => 'uid',
'apple.email' => 'mail',
);
**Table of Contents**
- [Apple as authsource](#apple-as-authsource)
- [Usage](#usage)
- [Creating Apple OAuth Client](#creating-apple-oauth-client)
# Apple as authsource
Apple provides own solution for Sign in with Apple, which is very similar to OAuth2, but without /userinfo endpoint
You need to use the `authoauth2:AppleAuth` authsource since Apple doesn't conform
the expected OIDC/OAuth pattern.
# Usage
```php
'apple' => [
'authoauth2:AppleAuth',
'clientId' => 'CLIENT_ID',
'clientSecret' => 'CLIENT_SECRET',
'redirectUri' => 'REDIRECT_URI',
//scopes: Only email is available
],
```
# Creating Apple OAuth Client
Apple provides [documentation](https://developer.apple.com/documentation/sign_in_with_apple/).
You will need to add the correct Callback URL to your OAuth2 client in the Apple console. Use a URL of the form below, and set hostname, SSP_PATH and optionally port to the correct values.
https://hostname/SSP_PATH/module.php/authoauth2/linkback.php
You will then need to change your `authsource` configuration to match the example usage above.
On your idp side you may need to use `apple2name` attribute mapping from this module.
```php
// Convert apple names to ldap friendly names
// apple.sub => uid, apple.email => mail
10 => array(
'class' => 'core:AttributeMap',
'authoauth2:apple2name'
),
```
<?php
namespace SimpleSAML\Module\authoauth2\Auth\Source;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Middleware;
use League\OAuth2\Client\Token\AccessTokenInterface;
use SimpleSAML\Logger;
use SimpleSAML\Module\authoauth2\Providers\AppleProvider;
class AppleAuth extends OAuth2
{
protected static $defaultProviderClass = AppleProvider::class;
public function __construct(array $info, array $config)
{
// Set some defaults
if (!array_key_exists('template', $config)) {
$config['template'] = 'Apple';
}
parent::__construct($info, $config);
}
/**
* Get the provider to use to talk to the OAuth2 server.
* Only visible for testing
*
* Since SSP may serialize Auth modules we don't assign the potentially unserializable provider to a field.
* @param \SimpleSAML\Configuration $config
* @return AppleProvider
*/
public function getProvider(\SimpleSAML\Configuration $config)
{
$providerLabel = $this->getLabel();
$collaborators = [];
if ($config->getBoolean('logHttpTraffic', false) === true) {
$format = $config->getString('logMessageFormat', self::DEBUG_LOG_FORMAT);
Logger::debug('authoauth2: Enable traffic logging');
$handlerStack = HandlerStack::create();
$handlerStack->push(
Middleware::log(new \SAML2\Compat\Ssp\Logger(), new MessageFormatter("authoauth2: $providerLabel $format")),
'logHttpTraffic'
);
$clientConfig = $config->toArray();
$clientConfig['handler'] = $handlerStack;
$client = new Client($clientConfig);
$collaborators['httpClient'] = $client;
}
return new AppleProvider($config->toArray(), $collaborators);
}
public function finalStep(array &$state, $oauth2Code)
{
$start = microtime(true);
$providerLabel = $this->getLabel();
$provider = $this->getProvider($this->config);
/**
* @var AccessTokenInterface $accessToken
*/
$accessToken = $this->retry(
function () use ($provider, $oauth2Code) {
return $provider->getAccessToken('authorization_code', [
'code' => $oauth2Code,
'grant_type' => 'authorization_code',
]);
},
$this->config->getInteger('retryOnError', 1)
);
$tokenAttributes = [];
if (array_key_exists('id_token', $accessToken->getValues())) {
$idToken = $accessToken->getValues()['id_token'];
$decodedIdToken = base64_decode(
explode('.', $idToken)[1]
);
$tokenAttributes = json_decode($decodedIdToken, true);
}
$attributes = [];
$fields = $provider->getTokenFieldsToUserDetailsUrl();
foreach ($fields as $field) {
if (isset($tokenAttributes[$field])) {
$attributes[$field] = $tokenAttributes[$field];
}
}
$prefix = $this->getAttributePrefix();
$state['Attributes'] = $this->convertResourceOwnerAttributes($attributes, $prefix);
$this->postFinalStep($accessToken, $provider, $state);
Logger::debug(
'authoauth2: ' . $providerLabel . ' attributes: ' . implode(", ", array_keys($state['Attributes']))
);
// Track time spent calling out to oauth2 server. This can often be a source of slowness.
$time = microtime(true) - $start;
Logger::debug('authoauth2: ' . $providerLabel . ' finished authentication in ' . $time . ' seconds'); }
}
......@@ -164,5 +164,18 @@ class ConfigTemplate
// Improve log lines
'label' => 'bitbucket'
];
const Apple = [
'authoauth2:AppleAuth',
'issuer' => 'https://appleid.apple.com',
'urlAuthorize' => 'https://appleid.apple.com/auth/authorize',
'urlAccessToken' => 'https://appleid.apple.com/auth/token',
//scopes are the default ones configured for your application
'attributePrefix' => 'apple.',
'scopes' => ['email'],
'scopeSeparator' => ' ',
// Improve log lines
'label' => 'apple'
];
}
// phpcs:enable
<?php
namespace SimpleSAML\Module\authoauth2\Providers;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class AppleProvider extends AdjustableGenericProvider
{
use BearerAuthorizationTrait;
/**
* Constructs an OAuth 2.0 service provider.
*
* @param array $options An array of options to set on this provider.
* Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
* Individual providers may introduce more options, as needed.
* @param array $collaborators An array of collaborators that may be used to
* override this provider's default behavior. Collaborators include
* `grantFactory`, `requestFactory`, and `httpClient`.
* Individual providers may introduce more collaborators, as needed.
*/
public function __construct(array $options = [], array $collaborators = [])
{
parent::__construct($options, $collaborators);
$this->tokenFieldsToUserDetailsUrl = ['sub', 'email', 'email_verified', 'is_private_email'];
}
/**
* Returns all options that are required.
*
* @return array
*/
protected function getRequiredOptions()
{
return [
'urlAuthorize',
'urlAccessToken',
];
}
/**
* Get the string used to separate scopes.
*
* @return string
*/
protected function getScopeSeparator()
{
return ' ';
}
public function getDefaultScopes()
{
return 'email';
}
public function getTokenFieldsToUserDetailsUrl() {
return $this->tokenFieldsToUserDetailsUrl;
}
/**
* Change response mode when scope requires it
*
* @param array $options
*
* @return array
*/
protected function getAuthorizationParameters(array $options)
{
$options = parent::getAuthorizationParameters($options);
$options['grant_type'] = 'authorization_code';
if (strpos($options['scope'], 'name') !== false || strpos($options['scope'], 'email') !== false) {
$options['response_mode'] = 'form_post';
}
return $options;
}
/**
* Builds the access token URL's query string.
*
* @param array $params Query parameters
* @return string Query string
*/
protected function getAccessTokenQuery(array $params)
{
$params['grant_type'] = 'authorization_code';
return $this->buildQueryString($params);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment