Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/meteor/app/apple/lib/handleIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { KJUR } from 'jsrsasign';
import { HTTP } from 'meteor/http';
import NodeRSA from 'node-rsa';

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

const key = applePublicKeys.find((k: any) => 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 KJUR.jws.JWS.verify(identityToken, userKey, ['RS256']);
} catch {
return false;
}
}

export function handleIdentityToken(identityToken: string): { id: string; email: string; name: string } {
const decodedToken = KJUR.jws.JWS.parse(identityToken);

if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) {
throw new Error('identityToken is not a valid JWT');
}

if (!decodedToken.payloadObj) {
throw new Error('identityToken does not have a payload');
}

const { iss, sub, email } = decodedToken.payloadObj as any;
if (!iss) {
throw new Error('Insufficient data in auth response token');
}

const serviceData = {
id: sub,
email,
name: '',
};

return serviceData;
}
63 changes: 13 additions & 50 deletions apps/meteor/app/apple/server/AppleCustomOAuth.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,35 @@
import { Accounts } from 'meteor/accounts-base';
import { HTTP } from 'meteor/http';
import NodeRSA from 'node-rsa';
import { KJUR } from 'jsrsasign';

import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server';
import { MeteorError } from '../../../server/sdk/errors';

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

const key = applePublicKeys.find((k: any) => 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 KJUR.jws.JWS.verify(identityToken, userKey, ['RS256']);
} catch {
return false;
}
};
import { handleIdentityToken } from '../lib/handleIdentityToken';

export class AppleCustomOAuth extends CustomOAuth {
getIdentity(_accessToken: string, query: Record<string, any>): any {
const { id_token: identityToken, user: userStr = '' } = query;

let user = {} as any;
let usrObj = {} as any;
try {
user = JSON.parse(userStr);
usrObj = JSON.parse(userStr);
} catch (e) {
// ignore
}

const decodedToken = KJUR.jws.JWS.parse(identityToken);
try {
const serviceData = handleIdentityToken(identityToken);

if (!isValidAppleJWT(identityToken, decodedToken.headerObj)) {
return {
type: 'apple',
error: new MeteorError(Accounts.LoginCancelledError.numericError, 'identityToken is a invalid JWT'),
};
}
if (usrObj?.name) {
serviceData.name = `${usrObj.name.firstName}${usrObj.name.middleName ? ` ${usrObj.name.middleName}` : ''}${
usrObj.name.lastName ? ` ${usrObj.name.lastName}` : ''
}`;
}

const { iss, sub, email } = decodedToken.payloadObj as any;
if (!iss) {
return serviceData;
} catch (error: any) {
return {
type: 'apple',
error: new MeteorError(Accounts.LoginCancelledError.numericError, 'Insufficient data in auth response token'),
error: new MeteorError(Accounts.LoginCancelledError.numericError, error.message),
};
}

const serviceData = {
id: sub,
email,
name: '',
};

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

if (user?.name) {
serviceData.name = `${user.name.firstName}${user.name.middleName ? ` ${user.name.middleName}` : ''}${
user.name.lastName ? ` ${user.name.lastName}` : ''
}`;
}

return serviceData;
}
}
17 changes: 16 additions & 1 deletion apps/meteor/app/apple/server/appleOauthRegisterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ settings.watchMultiple(
});
}

// if everything is empty but Apple login is enabled, don't show the login button
if (!clientId && !serverSecret && !iss && !kid) {
ServiceConfiguration.configurations.upsert(
{
service: 'apple',
},
{
$set: {
showButton: false,
enabled: settings.get('Accounts_OAuth_Apple'),
},
},
);
return;
}

const HEADER = {
kid,
alg: 'ES256',
Expand Down Expand Up @@ -67,7 +83,6 @@ settings.watchMultiple(
enabled: settings.get('Accounts_OAuth_Apple'),
loginStyle: 'popup',
clientId,
buttonLabelText: 'Sign in with Apple',
buttonColor: '#000',
buttonLabelColor: '#FFF',
},
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/apple/server/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import './appleOauthRegisterService';
import './loginHandler';
49 changes: 49 additions & 0 deletions apps/meteor/app/apple/server/loginHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

import { handleIdentityToken } from '../lib/handleIdentityToken';
import { settings } from '../../settings/server';

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

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

const { identityToken, fullName, email } = loginRequest;

try {
const serviceData = handleIdentityToken(identityToken);

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

const profile: { name?: string } = {};

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

const result = Accounts.updateOrCreateUserFromExternalService('apple', serviceData, { profile });

// 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;
} catch (error: any) {
return {
type: 'apple',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, error.message),
};
}
});
6 changes: 6 additions & 0 deletions apps/meteor/definition/externals/meteor/accounts-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ declare module 'meteor/accounts-base' {

function _runLoginHandlers<T>(methodInvocation: T, loginRequest: Record<string, any>): Record<string, any> | undefined;

function updateOrCreateUserFromExternalService(
serviceName: string,
serviceData: Record<string, unknown>,
options: Record<string, unknown>,
): Record<string, unknown>;

export class ConfigError extends Error {}

export class LoginCancelledError extends Error {
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"Accounts_LoginExpiration": "Login Expiration in Days",
"Accounts_ManuallyApproveNewUsers": "Manually Approve New Users",
"Accounts_OAuth_Apple": "Sign in with Apple",
"Accounts_OAuth_Apple_Description": "If you want Apple login enabled only on mobile, you can leave all fields empty.",
"Accounts_OAuth_Custom_Access_Token_Param": "Param Name for access token",
"Accounts_OAuth_Custom_Authorize_Path": "Authorize Path",
"Accounts_OAuth_Custom_Avatar_Field": "Avatar field",
Expand Down Expand Up @@ -5421,4 +5422,4 @@
"Device_Management_Email_Subject": "[Site_Name] - Login Detected",
"Device_Management_Email_Body": "You may use the following placeholders: <h2 class=\"rc-color\">{Login_Detected}</h2><p><strong>[name] ([username]) {Logged_In_Via}</strong></p><p><strong>{Device_Management_Client}:</strong> [browserInfo]<br><strong>{Device_Management_OS}:</strong> [osInfo]<br><strong>{Device_Management_Device}:</strong> [deviceInfo]<br><strong>{Device_Management_IP}:</strong>[ipInfo]</p><p><small>[userAgent]</small></p><a class=\"btn\" href=\"[Site_URL]\">{Access_Your_Account}</a><p>{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}<br><a href=\"[Site_URL]\">[SITE_URL]</a></p><p>{Thank_You_For_Choosing_RocketChat}</p>",
"Something_Went_Wrong": "Something went wrong"
}
}