Skip to content
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

[NEW] Sign in with apple (iOS client only) #18258

Merged
merged 10 commits into from
Jul 14, 2020
2 changes: 2 additions & 0 deletions app/apple/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './startup.js';
import './loginHandler.js';
33 changes: 33 additions & 0 deletions app/apple/server/loginHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

import { handleIdentityToken } from './tokenHandler';
import { settings } from '../../settings';

Accounts.registerLoginHandler('apple', (loginRequest) => {
if (!loginRequest.identityToken) {
return;
}

if (!settings.get('Accounts_OAuth_Apple')) {
return;
}

const identityResult = handleIdentityToken(loginRequest);

if (!identityResult.error) {
const result = Accounts.updateOrCreateUserFromExternalService('apple', identityResult.serviceData, identityResult.options);

// Ensure processing succeeded
if (result === undefined || result.userId === undefined) {
return {
type: 'apple',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Apple response token'),
};
}

return result;
}

return identityResult;
});
35 changes: 35 additions & 0 deletions app/apple/server/startup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';

import { settings } from '../../settings';

settings.addGroup('OAuth', function() {
this.section('Apple', function() {
this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true });
});
});

const configureService = _.debounce(Meteor.bindEnvironment(() => {
if (!settings.get('Accounts_OAuth_Apple')) {
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'),
},
});
}), 1000);

Meteor.startup(() => {
settings.get('Accounts_OAuth_Apple', () => {
configureService();
});
});
77 changes: 77 additions & 0 deletions app/apple/server/tokenHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { jws } from 'jsrsasign';
import NodeRSA from 'node-rsa';
import { HTTP } from 'meteor/http';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Match, check } from 'meteor/check';

const isValidAppleJWT = (identityToken, header) => {
const applePublicKeys = HTTP.get('https://appleid.apple.com/auth/keys').data.keys;
const { kid } = header;

const key = applePublicKeys.find((k) => k.kid === kid);

const pubKey = new NodeRSA();
pubKey.importKey(
{ n: Buffer.from(key.n, 'base64'), e: Buffer.from(key.e, 'base64') },
'components-public',
);
const userKey = pubKey.exportKey(['public']);

try {
return jws.JWS.verify(identityToken, userKey, {
typ: 'JWT',
alg: 'RS256',
});
} catch {
return false;
}
};

export const handleIdentityToken = ({ identityToken, fullName, email }) => {
check(identityToken, String);
check(fullName, Match.Maybe(Object));
check(email, Match.Maybe(String));

const decodedToken = jws.JWS.parse(identityToken);

if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) {
return {
type: 'apple',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'identityToken is a invalid JWT'),
};
}

const profile = {};

const { givenName, familyName } = fullName;
if (givenName && familyName) {
profile.name = `${ givenName } ${ familyName }`;
}

const { iss, iat, exp } = decodedToken.payloadObj;

if (!iss) {
return {
type: 'apple',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'Insufficient data in auth response token'),
};
}

// Collect basic auth provider details
const serviceData = {
id: iss,
did: iss.split(':').pop(),
issuedAt: new Date(iat * 1000),
expiresAt: new Date(exp * 1000),
};

if (email) {
serviceData.email = email;
}

return {
serviceData,
options: { profile },
};
};
1 change: 1 addition & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"Accounts_iframe_url": "Iframe URL",
"Accounts_LoginExpiration": "Login Expiration in Days",
"Accounts_ManuallyApproveNewUsers": "Manually Approve New Users",
"Accounts_OAuth_Apple": "Sign in with Apple",
"Accounts_OAuth_Custom_Authorize_Path": "Authorize Path",
"Accounts_OAuth_Custom_Avatar_Field": "Avatar field",
"Accounts_OAuth_Custom_Button_Color": "Button Color",
Expand Down
1 change: 1 addition & 0 deletions packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"Accounts_iframe_url": "URL do Iframe",
"Accounts_LoginExpiration": "Expiração do Login em Dias",
"Accounts_ManuallyApproveNewUsers": "Aprovar Manualmente novos Usuários",
"Accounts_OAuth_Apple": "Iniciar sessão com a Apple",
"Accounts_OAuth_Custom_Authorize_Path": "Path de Autorização",
"Accounts_OAuth_Custom_Avatar_Field": "Avatar",
"Accounts_OAuth_Custom_Button_Color": "Cor do Botão",
Expand Down
1 change: 1 addition & 0 deletions server/importPackages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '../app/sms';
import '../app/2fa/server';
import '../app/analytics/server';
import '../app/api/server';
import '../app/apple/server';
import '../app/assets/server';
import '../app/authorization';
import '../app/autolinker/server';
Expand Down