diff --git a/CHANGES.md b/CHANGES.md
index 0a7887c4c9..b04bc6b673 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,7 @@
## Features
+- New OAuth adapter for sign-in with Apple
- New route in Locale API to fetch list of languages
## Bug Fixes
diff --git a/app/app.php b/app/app.php
index 9fba836c24..a72116a767 100644
--- a/app/app.php
+++ b/app/app.php
@@ -104,6 +104,7 @@ $utopia->init(function () use ($utopia, $request, $response, &$user, $project, $
if(!$originValidator->isValid($origin)
&& in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
+ && $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('X-Appwrite-Key', ''))) {
throw new Exception($originValidator->getDescription(), 403);
}
diff --git a/app/config/providers.php b/app/config/providers.php
index 703bbef264..7eb5113d96 100644
--- a/app/config/providers.php
+++ b/app/config/providers.php
@@ -5,138 +5,182 @@ return [
'developers' => 'https://developer.atlassian.com/bitbucket',
'icon' => 'icon-bitbucket',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'facebook' => [
'developers' => 'https://developers.facebook.com/',
'icon' => 'icon-facebook',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'github' => [
'developers' => 'https://developer.github.com/',
'icon' => 'icon-github-circled',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'gitlab' => [
'developers' => 'https://docs.gitlab.com/ee/api/',
'icon' => 'icon-gitlab',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'google' => [
'developers' => 'https://developers.google.com/',
'icon' => 'icon-google',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
// 'instagram' => [
// 'developers' => 'https://www.instagram.com/developer/',
// 'icon' => 'icon-instagram',
// 'enabled' => false,
+ // 'beta' => false,
// 'mock' => false,
// ],
'microsoft' => [
'developers' => 'https://developer.microsoft.com/en-us/',
'icon' => 'icon-windows',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
// 'twitter' => [
// 'developers' => 'https://developer.twitter.com/',
// 'icon' => 'icon-twitter',
// 'enabled' => false,
+ // 'beta' => false,
// 'mock' => false,
// ],
'linkedin' => [
'developers' => 'https://developer.linkedin.com/',
'icon' => 'icon-linkedin',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'slack' => [
'developers' => 'https://api.slack.com/',
'icon' => 'icon-slack',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'dropbox' => [
'developers' => 'https://www.dropbox.com/developers/documentation',
'icon' => 'icon-dropbox',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'salesforce' => [
'developers' => 'https://developer.salesforce.com/docs/',
'icon' => 'icon-salesforce',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
+ 'mock' => false,
+ ],
+ 'apple' => [
+ 'developers' => 'https://developer.apple.com/',
+ 'icon' => 'icon-apple',
+ 'enabled' => true,
+ 'form' => 'apple.phtml', // Perperation for adding ability to customized OAuth UI forms, currently handled hardcoded.
+ 'beta' => true,
'mock' => false,
],
- // 'apple' => [
- // 'developers' => 'https://developer.apple.com/',
- // 'icon' => 'icon-apple',
- // 'enabled' => false,
- // 'mock' => false,
- // ],
'amazon' => [
'developers' => 'https://developer.amazon.com/apps-and-games/services-and-apis',
'icon' => 'icon-amazon',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'vk' => [
'developers' => 'https://vk.com/dev',
'icon' => 'icon-vk',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'discord' => [
'developers' => 'https://discordapp.com/developers/docs/topics/oauth2',
'icon' => 'icon-discord',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'twitch' => [
'developers' => 'https://dev.twitch.tv/docs/authentication',
'icon' => 'icon-twitch',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'spotify' => [
'developers' => 'https://developer.spotify.com/documentation/general/guides/authorization-guide/',
'icon' => 'icon-spotify',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'yahoo' => [
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
'icon' => 'icon-yahoo',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'yandex' => [
'developers' => 'https://tech.yandex.com/oauth/',
'icon' => 'icon-yandex',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false,
],
'twitter' => [
'developers' => 'https://developer.twitter.com/',
'icon' => 'icon-twitter',
'enabled' => false,
+ 'form' => false,
+ 'beta' => false,
'mock' => false
],
'paypal' => [
'developers' => 'https://developer.paypal.com/docs/api/overview/',
'icon' => 'icon-paypal',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false
],
'bitly' => [
'developers' => 'https://dev.bitly.com/v4_documentation.html',
'icon' => 'icon-bitly',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => false
],
// Keep Last
@@ -144,6 +188,8 @@ return [
'developers' => 'https://appwrite.io',
'icon' => 'icon-appwrite',
'enabled' => true,
+ 'form' => false,
+ 'beta' => false,
'mock' => true,
]
];
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index a2b7afe0fc..6e7d437fd1 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -308,6 +308,29 @@ $utopia->get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
}
);
+$utopia->post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
+ ->desc('OAuth2 Callback')
+ ->label('error', __DIR__.'/../../views/general/error.phtml')
+ ->label('scope', 'public')
+ ->label('origin', '*')
+ ->label('docs', false)
+ ->param('projectId', '', function () { return new Text(1024); }, 'Project unique ID.')
+ ->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'OAuth2 provider.')
+ ->param('code', '', function () { return new Text(1024); }, 'OAuth2 code.')
+ ->param('state', '', function () { return new Text(2048); }, 'Login state params.', true)
+ ->action(
+ function ($projectId, $provider, $code, $state) use ($response) {
+ $domain = Config::getParam('domain');
+ $protocol = Config::getParam('protocol');
+
+ $response
+ ->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
+ ->addHeader('Pragma', 'no-cache')
+ ->redirect($protocol.'://'.$domain.'/v1/account/sessions/oauth2/'.$provider.'/redirect?'
+ .http_build_query(['project' => $projectId, 'code' => $code, 'state' => $state]));
+ }
+ );
+
$utopia->get('/v1/account/sessions/oauth2/:provider/redirect')
->desc('OAuth2 Redirect')
->label('error', __DIR__.'/../../views/general/error.phtml')
@@ -361,6 +384,7 @@ $utopia->get('/v1/account/sessions/oauth2/:provider/redirect')
if (!empty($state['failure']) && !$validateURL->isValid($state['failure'])) {
throw new Exception('Invalid redirect URL for failure login', 400);
}
+
$state['failure'] = null;
$accessToken = $oauth2->getAccessToken($code);
diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php
index 54ccd2dcec..7cd14e583c 100644
--- a/app/controllers/api/projects.php
+++ b/app/controllers/api/projects.php
@@ -361,7 +361,7 @@ $utopia->patch('/v1/projects/:projectId/oauth2')
->param('projectId', '', function () { return new UID(); }, 'Project unique ID.')
->param('provider', '', function () { return new WhiteList(array_keys(Config::getParam('providers'))); }, 'Provider Name', false)
->param('appId', '', function () { return new Text(256); }, 'Provider app ID.', true)
- ->param('secret', '', function () { return new text(256); }, 'Provider secret key.', true)
+ ->param('secret', '', function () { return new text(512); }, 'Provider secret key.', true)
->action(
function ($projectId, $provider, $appId, $secret) use ($request, $response, $consoleDB) {
$project = $consoleDB->getDocument($projectId);
diff --git a/app/views/console/users/index.phtml b/app/views/console/users/index.phtml
index 62cf13a4b2..b7fbdf053a 100644
--- a/app/views/console/users/index.phtml
+++ b/app/views/console/users/index.phtml
@@ -337,12 +337,14 @@ $providers = $this->getParam('providers', []);
$data):
if (isset($data['enabled']) && !$data['enabled']) { continue; }
if (isset($data['mock']) && $data['mock']) { continue; }
+ $form = (isset($data['form'])) ? $data['form'] : false;
+ $beta = (isset($data['beta'])) ? $data['beta'] : false;
?>
-
OAuth2 Settings
+
escape(ucfirst($provider)); ?> OAuth2 Settings
+ {{console-project.usersOauth2escape(ucfirst($provider)); ?>Appid}} &&
+ {{console-project.usersOauth2escape(ucfirst($provider)); ?>Secret}}">
-
+ !{{console-project.usersOauth2escape(ucfirst($provider)); ?>Appid}} ||
+ !{{console-project.usersOauth2escape(ucfirst($provider)); ?>Secret}}">
+
-
+
-
+ (beta)
OAuth2 Developer Docs
diff --git a/gulpfile.js b/gulpfile.js
index a0be3a4058..b45d16bf7e 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -47,6 +47,7 @@ const configApp = {
'public/scripts/views/forms/move-down.js',
'public/scripts/views/forms/move-up.js',
'public/scripts/views/forms/nav.js',
+ 'public/scripts/views/forms/oauth-apple.js',
'public/scripts/views/forms/password-meter.js',
'public/scripts/views/forms/pell.js',
'public/scripts/views/forms/remove.js',
diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js
index f7b6ba5a93..3dac3fcbd5 100644
--- a/public/dist/scripts/app-all.js
+++ b/public/dist/scripts/app-all.js
@@ -2568,7 +2568,7 @@ return"< 1s";}).add("markdown",function($value,markdown){return markdown.render(
let thresh=1000;if(Math.abs($value)=thresh&&u'+
units[u]+"");}).add("statsTotal",function($value){if(!$value){return 0;}
-$value=abbreviate($value,1,false,false);return $value==="0"?"N/A":$value;}).add("isEmpty",function($value){return(!!$value);}).add("isEmptyObject",function($value){return((Object.keys($value).length===0&&$value.constructor===Object)||$value.length===0)}).add("activeDomainsCount",function($value){let result=[];if(Array.isArray($value)){result=$value.filter(function(node){return(node.verification&&node.certificateId);});}
+$value=abbreviate($value,0,false,false);return $value==="0"?"N/A":$value;}).add("isEmpty",function($value){return(!!$value);}).add("isEmptyObject",function($value){return((Object.keys($value).length===0&&$value.constructor===Object)||$value.length===0)}).add("activeDomainsCount",function($value){let result=[];if(Array.isArray($value)){result=$value.filter(function(node){return(node.verification&&node.certificateId);});}
return result.length;}).add("documentAction",function(container){let collection=container.get('project-collection');let document=container.get('project-document');if(collection&&document&&!document.$id){return'database.createDocument';}
return'database.updateDocument';}).add("documentSuccess",function(container){let document=container.get('project-document');if(document&&!document.$id){return',redirect';}
return'';}).add("firstElement",function($value){if($value&&$value[0]){return $value[0];}
@@ -2640,7 +2640,11 @@ list["filters-"+filter.key]=params[key][i];}}}}
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
-console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-password-meter",controller:function(element,window){var calc=function(password){var score=0;if(!password)return score;var letters=new window.Object();for(var i=0;i60)return(meter.className="password-meter strong");if(score>30)return(meter.className="password-meter medium");if(score>=0)return(meter.className="password-meter weak");};var meter=window.document.createElement("div");meter.className="password-meter";element.parentNode.insertBefore(meter,element.nextSibling);element.addEventListener("change",callback);element.addEventListener("keypress",callback);element.addEventListener("keyup",callback);element.addEventListener("keydown",callback);}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-pell",controller:function(element,window,document,markdown,rtl){var div=document.createElement("div");element.className="pell hide";div.className="input pell";element.parentNode.insertBefore(div,element);element.tabIndex=-1;var turndownService=new TurndownService();turndownService.addRule("underline",{filter:["u"],replacement:function(content){return"__"+content+"__";}});var editor=window.pell.init({element:div,onChange:function onChange(html){alignText();element.value=turndownService.turndown(html);},defaultParagraphSeparator:"p",actions:[{name:"bold",icon:''},{name:"underline",icon:''},{name:"italic",icon:''},{name:"olist",icon:''},{name:"ulist",icon:''},{name:"link",icon:''}]});var clean=function(e){e.stopPropagation();e.preventDefault();var clipboardData=e.clipboardData||window.clipboardData;console.log(clipboardData.getData("Text"));window.pell.exec("insertText",clipboardData.getData("Text"));return true;};var alignText=function(){let paragraphs=editor.content.querySelectorAll('p,li');let last='';for(let paragraph of paragraphs){var content=paragraph.textContent;if(content.trim()===''){content=last.textContent;}
if(rtl.isRTL(content)){paragraph.style.direction='rtl';paragraph.style.textAlign='right';}
diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js
index 6643c8f322..15077e480e 100644
--- a/public/dist/scripts/app.js
+++ b/public/dist/scripts/app.js
@@ -284,7 +284,7 @@ return"< 1s";}).add("markdown",function($value,markdown){return markdown.render(
let thresh=1000;if(Math.abs($value)=thresh&&u'+
units[u]+"");}).add("statsTotal",function($value){if(!$value){return 0;}
-$value=abbreviate($value,1,false,false);return $value==="0"?"N/A":$value;}).add("isEmpty",function($value){return(!!$value);}).add("isEmptyObject",function($value){return((Object.keys($value).length===0&&$value.constructor===Object)||$value.length===0)}).add("activeDomainsCount",function($value){let result=[];if(Array.isArray($value)){result=$value.filter(function(node){return(node.verification&&node.certificateId);});}
+$value=abbreviate($value,0,false,false);return $value==="0"?"N/A":$value;}).add("isEmpty",function($value){return(!!$value);}).add("isEmptyObject",function($value){return((Object.keys($value).length===0&&$value.constructor===Object)||$value.length===0)}).add("activeDomainsCount",function($value){let result=[];if(Array.isArray($value)){result=$value.filter(function(node){return(node.verification&&node.certificateId);});}
return result.length;}).add("documentAction",function(container){let collection=container.get('project-collection');let document=container.get('project-document');if(collection&&document&&!document.$id){return'database.createDocument';}
return'database.updateDocument';}).add("documentSuccess",function(container){let document=container.get('project-document');if(document&&!document.$id){return',redirect';}
return'';}).add("firstElement",function($value){if($value&&$value[0]){return $value[0];}
@@ -356,7 +356,11 @@ list["filters-"+filter.key]=params[key][i];}}}}
return list;};let apply=function(params){let cached=container.get(name);cached=cached?cached.params:[];params=Object.assign(cached,params);container.set(name,{name:name,params:params,query:serialize(params),forward:parseInt(params.offset)+parseInt(params.limit),backward:parseInt(params.offset)-parseInt(params.limit),keys:flatten(params)},true,name);document.dispatchEvent(new CustomEvent(name+"-changed",{bubbles:false,cancelable:true}));};switch(element.tagName){case"INPUT":break;case"TEXTAREA":break;case"BUTTON":element.addEventListener("click",function(){apply(JSON.parse(expression.parse(element.dataset["params"]||"{}")));});break;case"FORM":element.addEventListener("input",function(){apply(form.toJson(element));});element.addEventListener("change",function(){apply(form.toJson(element));});element.addEventListener("reset",function(){setTimeout(function(){apply(form.toJson(element));},0);});events=events.trim().split(",");for(let y=0;y=distance)&&(distance>=0)){if(minLink){minLink.classList.remove('selected');}
-console.log('old',minLink);minDistance=distance;minElement=title;minLink=links[i];minLink.classList.add('selected');console.log('new',minLink);}}};window.addEventListener('scroll',check);check();}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-password-meter",controller:function(element,window){var calc=function(password){var score=0;if(!password)return score;var letters=new window.Object();for(var i=0;i60)return(meter.className="password-meter strong");if(score>30)return(meter.className="password-meter medium");if(score>=0)return(meter.className="password-meter weak");};var meter=window.document.createElement("div");meter.className="password-meter";element.parentNode.insertBefore(meter,element.nextSibling);element.addEventListener("change",callback);element.addEventListener("keypress",callback);element.addEventListener("keyup",callback);element.addEventListener("keydown",callback);}});})(window);(function(window){"use strict";window.ls.container.get("view").add({selector:"data-forms-pell",controller:function(element,window,document,markdown,rtl){var div=document.createElement("div");element.className="pell hide";div.className="input pell";element.parentNode.insertBefore(div,element);element.tabIndex=-1;var turndownService=new TurndownService();turndownService.addRule("underline",{filter:["u"],replacement:function(content){return"__"+content+"__";}});var editor=window.pell.init({element:div,onChange:function onChange(html){alignText();element.value=turndownService.turndown(html);},defaultParagraphSeparator:"p",actions:[{name:"bold",icon:''},{name:"underline",icon:''},{name:"italic",icon:''},{name:"olist",icon:''},{name:"ulist",icon:''},{name:"link",icon:''}]});var clean=function(e){e.stopPropagation();e.preventDefault();var clipboardData=e.clipboardData||window.clipboardData;console.log(clipboardData.getData("Text"));window.pell.exec("insertText",clipboardData.getData("Text"));return true;};var alignText=function(){let paragraphs=editor.content.querySelectorAll('p,li');let last='';for(let paragraph of paragraphs){var content=paragraph.textContent;if(content.trim()===''){content=last.textContent;}
if(rtl.isRTL(content)){paragraph.style.direction='rtl';paragraph.style.textAlign='right';}
diff --git a/public/images/oauth2/apple.png b/public/images/oauth2/apple.png
index b0fdfd14f0..98086e8048 100644
Binary files a/public/images/oauth2/apple.png and b/public/images/oauth2/apple.png differ
diff --git a/public/scripts/views/forms/oauth-apple.js b/public/scripts/views/forms/oauth-apple.js
new file mode 100644
index 0000000000..358e9aae86
--- /dev/null
+++ b/public/scripts/views/forms/oauth-apple.js
@@ -0,0 +1,94 @@
+(function(window) {
+ "use strict";
+
+ window.ls.container.get("view").add({
+ selector: "data-forms-oauth-apple",
+ controller: function(element) {
+ let container = document.createElement("div");
+ let row = document.createElement("div");
+ let col1 = document.createElement("div");
+ let col2 = document.createElement("div");
+ let keyID = document.createElement("input");
+ let keyLabel = document.createElement("label");
+ let teamID = document.createElement("input");
+ let teamLabel = document.createElement("label");
+ let p8 = document.createElement("textarea");
+ let p8Label = document.createElement("label");
+
+ keyLabel.textContent = 'Key ID';
+ teamLabel.textContent = 'Team ID';
+ p8Label.textContent = 'P8 File';
+
+ row.classList.add('row');
+ row.classList.add('thin');
+ container.appendChild(row);
+ container.appendChild(p8Label);
+ container.appendChild(p8);
+
+ row.appendChild(col1);
+ row.appendChild(col2);
+
+ col1.classList.add('col');
+ col1.classList.add('span-6');
+ col1.appendChild(keyLabel);
+ col1.appendChild(keyID);
+
+ col2.classList.add('col');
+ col2.classList.add('span-6');
+ col2.appendChild(teamLabel);
+ col2.appendChild(teamID);
+
+ keyID.type = 'text';
+ keyID.placeholder = 'SHAB13ROFN';
+ teamID.type = 'text';
+ teamID.placeholder = 'ELA2CD3AED';
+ p8.accept = '.p8';
+ p8.classList.add('margin-bottom-no');
+
+ element.parentNode.insertBefore(container, element.nextSibling);
+
+ element.addEventListener('change', sync);
+ keyID.addEventListener('change', update);
+ teamID.addEventListener('change', update);
+ p8.addEventListener('change', update);
+
+ function update() {
+ let json = {};
+
+ json.keyID = keyID.value;
+ json.teamID = teamID.value;
+ json.p8 = p8.value;
+
+ element.value = JSON.stringify(json);
+ }
+
+ function sync() {
+ console.log('sync');
+ if(!element.value) {
+ return;
+ }
+
+ let json = {};
+
+ try {
+ json = JSON.parse(element.value);
+ } catch (error) {
+ console.error('Failed to parse secret key');
+ }
+
+ teamID.value = json.teamID || '';
+ keyID.value = json.keyID || '';
+ p8.value = json.p8 || '';
+ }
+
+ // function syncB() {
+ // picker.value = element.value;
+ // }
+
+ // element.parentNode.insertBefore(preview, element);
+
+ // update();
+ sync();
+ }
+ });
+})(window);
diff --git a/src/Appwrite/Auth/OAuth2/Apple.php b/src/Appwrite/Auth/OAuth2/Apple.php
index 427b78debb..411f8baf59 100644
--- a/src/Appwrite/Auth/OAuth2/Apple.php
+++ b/src/Appwrite/Auth/OAuth2/Apple.php
@@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
+use Exception;
// Reference Material
// https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
@@ -22,6 +23,11 @@ class Apple extends OAuth2
"email"
];
+ /**
+ * @var array
+ */
+ protected $claims = [];
+
/**
* @return string
*/
@@ -58,15 +64,18 @@ class Apple extends OAuth2
'https://appleid.apple.com/auth/token',
$headers,
http_build_query([
+ 'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => $this->appID,
- 'client_secret' => $this->appSecret,
+ 'client_secret' => $this->getAppSecret(),
'redirect_uri' => $this->callback,
- 'grant_type' => 'authorization_code'
])
);
- $accessToken = json_decode($accessToken, true);
+ $accessToken = json_decode($accessToken, true);
+
+ $this->claims = (isset($accessToken['id_token'])) ? explode('.', $accessToken['id_token']) : [0 => '', 1 => ''];
+ $this->claims = (isset($this->claims[1])) ? json_decode(base64_decode($this->claims[1]), true) : [];
if (isset($accessToken['access_token'])) {
return $accessToken['access_token'];
@@ -82,10 +91,8 @@ class Apple extends OAuth2
*/
public function getUserID(string $accessToken): string
{
- $user = $this->getUser($accessToken);
-
- if (isset($user['account_id'])) {
- return $user['account_id'];
+ if (isset($this->claims['sub']) && !empty($this->claims['sub'])) {
+ return $this->claims['sub'];
}
return '';
@@ -98,10 +105,11 @@ class Apple extends OAuth2
*/
public function getUserEmail(string $accessToken): string
{
- $user = $this->getUser($accessToken);
-
- if (isset($user['email'])) {
- return $user['email'];
+ if (isset($this->claims['email']) &&
+ !empty($this->claims['email']) &&
+ isset($this->claims['email_verified']) &&
+ $this->claims['email_verified'] === 'true') {
+ return $this->claims['email'];
}
return '';
@@ -114,28 +122,111 @@ class Apple extends OAuth2
*/
public function getUserName(string $accessToken): string
{
- $user = $this->getUser($accessToken);
-
- if (isset($user['name'])) {
- return $user['name']['display_name'];
+ if (isset($this->claims['email']) &&
+ !empty($this->claims['email']) &&
+ isset($this->claims['email_verified']) &&
+ $this->claims['email_verified'] === 'true') {
+ return $this->claims['email'];
}
return '';
}
- /**
- * @param string $accessToken
- *
- * @return array
- */
- protected function getUser(string $accessToken): array
+ protected function getAppSecret():string
{
- if (empty($this->user)) {
- $headers[] = 'Authorization: Bearer '. urlencode($accessToken);
- $user = $this->request('POST', '', $headers);
- $this->user = json_decode($user, true);
+ try {
+ $secret = json_decode($this->appSecret, true);
+ } catch (\Throwable $th) {
+ throw new Exception('Invalid secret');
}
- return $this->user;
+ $keyfile = (isset($secret['p8'])) ? $secret['p8'] : ''; // Your p8 Key file
+ $keyID = (isset($secret['keyID'])) ? $secret['keyID'] : ''; // Your Key ID
+ $teamID = (isset($secret['teamID'])) ? $secret['teamID'] : ''; // Your Team ID (see Developer Portal)
+ $bundleID = $this->appID; // Your Bundle ID
+
+ $headers = [
+ 'alg' => 'ES256',
+ 'kid' => $keyID,
+ ];
+
+ $claims = [
+ 'iss' => $teamID,
+ 'iat' => time(),
+ 'exp' => time() + 86400*180,
+ 'aud' => 'https://appleid.apple.com',
+ 'sub' => $bundleID,
+ ];
+
+ $pkey = openssl_pkey_get_private($keyfile);
+
+ $payload = $this->encode(json_encode($headers)).'.'.$this->encode(json_encode($claims));
+
+ $signature = '';
+
+ $success = openssl_sign($payload, $signature, $pkey, OPENSSL_ALGO_SHA256);
+
+ if (!$success) return '';
+
+ return $payload.'.'.$this->encode($this->fromDER($signature, 64));
+ }
+
+ /**
+ * @param string $data
+ */
+ protected function encode($data)
+ {
+ return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
+ }
+
+ /**
+ * @param string $data
+ */
+ protected function retrievePositiveInteger(string $data): string
+ {
+ while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f') {
+ $data = mb_substr($data, 2, null, '8bit');
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param string $der
+ * @param int $partLength
+ */
+ protected function fromDER(string $der, int $partLength):string
+ {
+ $hex = \unpack('H*', $der)[1];
+
+ if ('30' !== \mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE
+ throw new \RuntimeException();
+ }
+
+ if ('81' === \mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128
+ $hex = \mb_substr($hex, 6, null, '8bit');
+ }
+ else {
+ $hex = \mb_substr($hex, 4, null, '8bit');
+ }
+ if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER
+ throw new \RuntimeException();
+ }
+
+ $Rl = \hexdec(\mb_substr($hex, 2, 2, '8bit'));
+ $R = $this->retrievePositiveInteger(\mb_substr($hex, 4, $Rl * 2, '8bit'));
+ $R = \str_pad($R, $partLength, '0', STR_PAD_LEFT);
+
+ $hex = \mb_substr($hex, 4 + $Rl * 2, null, '8bit');
+
+ if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER
+ throw new \RuntimeException();
+ }
+
+ $Sl = \hexdec(\mb_substr($hex, 2, 2, '8bit'));
+ $S = $this->retrievePositiveInteger(\mb_substr($hex, 4, $Sl * 2, '8bit'));
+ $S = \str_pad($S, $partLength, '0', STR_PAD_LEFT);
+
+ return \pack('H*', $R.$S);
}
}