[NEW] Apple Login (#24060)

Co-authored-by: Diego Sampaio <chinello@gmail.com>
pull/24075/head^2
Guilherme Gazzo 4 years ago committed by GitHub
parent 261c9e1ce1
commit 1441feb667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 76
      app/apple/server/appleOauthRegisterService.ts
  2. 31
      app/apple/server/startup.ts
  3. 2
      app/apple/server/tokenHandler.js
  4. 4
      app/cors/server/cors.js
  5. 23
      app/ui-login/client/login/services.html
  6. 4
      client/lib/utils/createAnchor.ts
  7. 4
      client/templates.ts
  8. 93
      client/views/login/AppleOauth/AppleOauthButton.tsx
  9. 2
      definition/rest/v1/settings.ts
  10. 6
      package-lock.json
  11. 1
      package.json
  12. 2
      server/configuration/accounts_meld.js

@ -0,0 +1,76 @@
import { KJUR } from 'jsrsasign';
import { ServiceConfiguration } from 'meteor/service-configuration';
import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server';
import { settings, settingsRegistry } from '../../settings/server';
const config = {
serverURL: 'https://appleid.apple.com',
tokenPath: '/auth/token',
scope: 'name email',
mergeUsers: true,
accessTokenParam: 'access_token',
loginStyle: 'popup',
};
new CustomOAuth('apple', config);
settingsRegistry.addGroup('OAuth', function () {
this.section('Apple', function () {
this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true });
this.add('Accounts_OAuth_Apple_id', '', { type: 'string', public: true });
this.add('Accounts_OAuth_Apple_secretKey', '', { type: 'string', multiline: true });
this.add('Accounts_OAuth_Apple_iss', '', { type: 'string' });
this.add('Accounts_OAuth_Apple_kid', '', { type: 'string' });
});
});
settings.watchMultiple(
[
'Accounts_OAuth_Apple',
'Accounts_OAuth_Apple_id',
'Accounts_OAuth_Apple_secretKey',
'Accounts_OAuth_Apple_iss',
'Accounts_OAuth_Apple_kid',
],
([enabled, clientId, serverSecret, iss, kid]) => {
if (!enabled) {
return ServiceConfiguration.configurations.remove({
service: 'apple',
});
}
const HEADER = {
kid,
alg: 'ES256',
};
const tokenPayload = {
iss,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
aud: 'https://appleid.apple.com',
sub: clientId,
};
const secret = KJUR.jws.JWS.sign(null, HEADER, tokenPayload, serverSecret as string);
ServiceConfiguration.configurations.upsert(
{
service: 'apple',
},
{
$set: {
// We'll hide this button on Web Client
showButton: false,
secret,
enabled: settings.get('Accounts_OAuth_Apple'),
loginStyle: 'popup',
clientId,
},
},
);
},
);

@ -1,30 +1 @@
import { ServiceConfiguration } from 'meteor/service-configuration';
import { settings, settingsRegistry } from '../../settings/server';
settingsRegistry.addGroup('OAuth', function () {
this.section('Apple', function () {
this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true });
});
});
settings.watch('Accounts_OAuth_Apple', (enabled) => {
if (!enabled) {
return ServiceConfiguration.configurations.remove({
service: 'apple',
});
}
ServiceConfiguration.configurations.upsert(
{
service: 'apple',
},
{
$set: {
// We'll hide this button on Web Client
showButton: false,
enabled: settings.get('Accounts_OAuth_Apple'),
},
},
);
});
import './appleOauthRegisterService';

@ -25,7 +25,7 @@ const isValidAppleJWT = (identityToken, header) => {
}
};
export const handleIdentityToken = ({ identityToken, fullName, email }) => {
export const handleIdentityToken = ({ identityToken, fullName = {}, email }) => {
check(identityToken, String);
check(fullName, Match.Maybe(Object));
check(email, Match.Maybe(String));

@ -35,7 +35,7 @@ WebApp.rawConnectHandlers.use(function (req, res, next) {
]
.filter(Boolean)
.join(' ');
const external = [settings.get('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com'].filter(Boolean).join(' ');
res.setHeader(
'Content-Security-Policy',
[
@ -45,7 +45,7 @@ WebApp.rawConnectHandlers.use(function (req, res, next) {
'frame-src *',
'img-src * data: blob:',
'media-src * data:',
`script-src 'self' 'unsafe-eval' ${inlineHashes} ${cdn_prefixes}`,
`script-src 'self' 'unsafe-eval' ${inlineHashes} ${cdn_prefixes} ${external}`,
`style-src 'self' 'unsafe-inline' ${cdn_prefixes}`,
].join('; '),
);

@ -1,9 +1,18 @@
<template name='loginServices'>
<template name="loginServices">
{{#if loginService.length}}
<div class="oauth-login buttons-group">
{{#each loginService}}
<button type="button" class="rc-button rc-button--secondary external-login {{service.service}}" title="{{displayName}}" style="{{#if service.buttonColor}}background-color:{{service.buttonColor}};{{/if}}{{#if service.buttonLabelColor}}color:{{service.buttonLabelColor}};{{/if}}"><i class="icon-{{icon}} service-icon"></i><i class="icon-spin animate-spin loading-icon hidden"></i><span>{{service.buttonLabelText}}</span></button>
{{/each}}
</div>
{{/if}}
<div class="oauth-login buttons-group">
{{#each loginService}}
<button
type="button"
class="rc-button rc-button--secondary external-login {{service.service}}"
title="{{displayName}}"
style="{{#if service.buttonColor}}background-color:{{service.buttonColor}};{{/if}}{{#if service.buttonLabelColor}}color:{{service.buttonLabelColor}};{{/if}}"
>
<i class="icon-{{icon}} service-icon"></i
><i class="icon-spin animate-spin loading-icon hidden"></i
><span>{{service.buttonLabelText}}</span>
</button>
{{/each}}
</div>
{{/if}} {{> AppleOauthButton}}
</template>

@ -1,6 +1,6 @@
export const createAnchor = (id: string): HTMLDivElement => {
export const createAnchor = (id: string, target = document.body): HTMLDivElement => {
const div = document.createElement('div');
div.id = id;
document.body.appendChild(div);
target.appendChild(div);
return div;
};

@ -160,6 +160,10 @@ createTemplateForComponent('SortList', () => import('./components/SortList'));
createTemplateForComponent('CreateRoomList', () => import('./sidebar/header/actions/CreateRoomList'));
createTemplateForComponent('AppleOauthButton', () => import('./views/login/AppleOauth/AppleOauthButton'), {
renderContainerView: () => HTML.DIV({ style: 'display: flex; justify-content: center;' }),
});
createTemplateForComponent('UserDropdown', () => import('./sidebar/header/UserDropdown'));
createTemplateForComponent('sidebarFooter', () => import('./sidebar/footer'));

@ -0,0 +1,93 @@
import { Accounts } from 'meteor/accounts-base';
import React, { FC, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { useAbsoluteUrl } from '../../../contexts/ServerContext';
import { useSetting } from '../../../contexts/SettingsContext';
export const AppleOauthButton: FC = () => {
const enabled = useSetting('Accounts_OAuth_Apple');
const absoluteUrl = useAbsoluteUrl();
const appleClientID = useSetting('Accounts_OAuth_Apple_id');
const redirectURI = absoluteUrl('_oauth/apple');
useEffect(() => {
const success = (data: any): void => {
const { authorization, user } = data.detail;
Accounts.callLoginMethod({
methodArguments: [
{
serviceName: 'apple',
identityToken: authorization.id_token,
...(user && {
fullName: {
givenName: user.name.firstName,
familyName: user.name.lastName,
},
email: user.email,
}),
},
],
userCallback: console.log,
});
};
const error = (error: any): void => {
// handle error.
console.error(error);
};
document.addEventListener('AppleIDSignInOnSuccess', success);
// Listen for authorization failures
document.addEventListener('AppleIDSignInOnFailure', error);
return (): void => {
document.removeEventListener('AppleIDSignInOnSuccess', success);
document.removeEventListener('AppleIDSignInOnFailure', error);
};
}, []);
const scriptLoadedHandler = useCallback(() => {
if (!enabled) {
return;
}
(window as any).AppleID.auth.init({
clientId: appleClientID,
scope: 'name email',
redirectURI,
usePopup: true,
});
}, [enabled, appleClientID, redirectURI]);
const ref = useRef<HTMLScriptElement>();
useEffect(() => {
if ((window as any).AppleID) {
scriptLoadedHandler();
return;
}
if (!ref.current) {
return;
}
ref.current.onload = scriptLoadedHandler;
}, [scriptLoadedHandler]);
useLayoutEffect(() => {
const script = document.createElement('script');
script.src = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
script.async = true;
ref.current = script;
document.body.appendChild(script);
return (): void => {
document.body.removeChild(script);
};
}, []);
if (!enabled) {
return null;
}
return <div id='appleid-signin' data-height='40px'></div>;
};
export default AppleOauthButton;

@ -17,7 +17,7 @@ export type OauthCustomConfiguration = {
identityPath: unknown;
authorizePath: unknown;
scope: unknown;
loginStyle: unknown;
loginStyle: 'popup' | 'redirect';
tokenSentVia: unknown;
identityTokenSentVia: unknown;
keyField: unknown;

6
package-lock.json generated

@ -10090,6 +10090,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/jsrsasign": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/jsrsasign/-/jsrsasign-9.0.1.tgz",
"integrity": "sha512-84hBaNlih+u0xuTw6i3rfZSVrXGDg8+SoY81YgTUGet3B32HrZjHBTx+GU8UTKbx3KrRUFnZiqX//pIZ3eFgfA==",
"dev": true
},
"@types/ldapjs": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.1.tgz",

@ -88,6 +88,7 @@
"@types/imap": "^0.8.35",
"@types/jsdom": "^16.2.12",
"@types/jsdom-global": "^3.0.2",
"@types/jsrsasign": "^9.0.1",
"@types/ldapjs": "^2.2.1",
"@types/less": "^3.0.2",
"@types/lodash.get": "^4.4.6",

@ -6,7 +6,7 @@ import { Users } from '../../app/models';
const orig_updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService;
Accounts.updateOrCreateUserFromExternalService = function (serviceName, serviceData = {}, ...args /* , options*/) {
const services = ['facebook', 'github', 'gitlab', 'google', 'meteor-developer', 'linkedin', 'twitter'];
const services = ['facebook', 'github', 'gitlab', 'google', 'meteor-developer', 'linkedin', 'twitter', 'apple'];
if (services.includes(serviceName) === false && serviceData._OAuthCustom !== true) {
return orig_updateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args]);

Loading…
Cancel
Save