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; ?>
  • + {{console-project.usersOauth2escape(ucfirst($provider)); ?>Appid}} && + {{console-project.usersOauth2escape(ucfirst($provider)); ?>Secret}}"> - + !{{console-project.usersOauth2escape(ucfirst($provider)); ?>Appid}} || + !{{console-project.usersOauth2escape(ucfirst($provider)); ?>Secret}}"> + - <?php echo ucfirst($provider); ?> Logo + <?php echo $this->escape(ucfirst($provider)); ?> Logo - + (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); } }