0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-03 22:15:32 -05:00

Merge pull request #973 from logto-io/gao-update-apple-connector

refactor(connector): remove apple js sdk
This commit is contained in:
Gao Sun 2022-05-28 22:08:50 +08:00 committed by GitHub
commit 8d17f9d73a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 24 additions and 151 deletions

View file

@ -2,7 +2,7 @@ import { GetConnectorConfig } from '@logto/connector-types';
import nock from 'nock';
import AppleConnector from '.';
import { defaultMetadata } from './constant';
import { authorizationEndpoint } from './constant';
import { mockedConfig } from './mock';
import { AppleConfig } from './types';
@ -25,7 +25,7 @@ describe('getAuthorizationUri', () => {
'http://localhost:3000/callback'
);
expect(authorizationUri).toEqual(
`${defaultMetadata.target}://?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=&state=some_state`
`${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=&state=some_state&response_type=code+id_token&response_mode=fragment`
);
});
});

View file

@ -9,10 +9,9 @@ import {
SocialConnector,
GetConnectorConfig,
} from '@logto/connector-types';
import { conditional } from '@silverhand/essentials';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { scope, defaultMetadata, jwksUri, issuer } from './constant';
import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from './constant';
import { appleConfigGuard, AppleConfig, appleDataGuard } from './types';
// TO-DO: support nonce validation
@ -37,9 +36,12 @@ export default class AppleConnector implements SocialConnector<string> {
redirect_uri: redirectUri,
scope,
state,
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
response_type: 'code id_token',
response_mode: 'fragment',
});
return `${this.metadata.target}://?${queryParameters.toString()}`;
return `${authorizationEndpoint}?${queryParameters.toString()}`;
};
// Directly return now. Refactor with connector interface redesign.
@ -50,10 +52,7 @@ export default class AppleConnector implements SocialConnector<string> {
// Extract data from JSON string.
// Refactor with connector interface redesign.
public getUserInfo: GetUserInfo<string> = async (data) => {
const {
authorization: { id_token: idToken },
user,
} = appleDataGuard.parse(JSON.parse(data));
const { id_token: idToken } = appleDataGuard.parse(JSON.parse(data));
if (!idToken) {
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
@ -71,13 +70,8 @@ export default class AppleConnector implements SocialConnector<string> {
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
}
const name = [user?.name?.firstName, user?.name?.lastName]
.filter((value) => Boolean(value))
.join(' ');
return {
id: payload.sub,
name: conditional(name),
};
} catch {
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);

View file

@ -8,19 +8,7 @@ export type AppleConfig = z.infer<typeof appleConfigGuard>;
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
export const appleDataGuard = z.object({
authorization: z.object({
id_token: z.string(),
}),
user: z
.object({
name: z
.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
})
.optional(),
})
.optional(),
id_token: z.string(),
});
export type AppleData = z.infer<typeof appleDataGuard>;

View file

@ -40,8 +40,14 @@ export const getUserInfoByAuthCode = async (
authCode: string,
redirectUri: string
): Promise<SocialUserInfo> => {
// TO-DO: rename and refactor connector methods
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = JSON.parse(authCode);
const connector = await getConnector(connectorId);
const accessTokenObject = await connector.getAccessToken(authCode, redirectUri);
const accessTokenObject = await connector.getAccessToken(
Object.keys(data).length > 1 ? authCode : data.code,
redirectUri
);
return connector.getUserInfo(accessTokenObject);
};

View file

@ -4,7 +4,6 @@ import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import * as styles from './App.module.scss';
import AppContent from './components/AppContent';
import { loadAppleSdk } from './hooks/use-apple-auth';
import usePageContext from './hooks/use-page-context';
import usePreview from './hooks/use-preview';
import initI18n from './i18n/init';
@ -17,7 +16,7 @@ import SecondarySignIn from './pages/SecondarySignIn';
import SignIn from './pages/SignIn';
import SocialRegister from './pages/SocialRegister';
import SocialSignInCallback from './pages/SocialSignInCallback';
import getSignInExperienceSettings, { isAppleConnectorEnabled } from './utils/sign-in-experience';
import getSignInExperienceSettings from './utils/sign-in-experience';
import './scss/normalized.scss';
@ -36,11 +35,6 @@ const App = () => {
(async () => {
const settings = await getSignInExperienceSettings();
// Load Apple official SDK if Apple connector is enabled
if (isAppleConnectorEnabled(settings)) {
await loadAppleSdk();
}
// Note: i18n must be initialized ahead of global experience settings
await initI18n(settings.languageInfo);

View file

@ -1,92 +0,0 @@
import camelcaseKeys from 'camelcase-keys';
import { useNavigate } from 'react-router-dom';
import { inOperator, parseQueryParameters } from '@/utils';
export const loadAppleSdk = async () =>
new Promise((resolve, reject) => {
const script = document.createElement('script');
script.addEventListener('load', resolve);
script.addEventListener('error', reject);
// eslint-disable-next-line @silverhand/fp/no-mutation
script.src =
'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
document.head.append(script);
});
export const isAppleConnector = ({ target }: { target: string }) => target === 'apple';
// Derived from https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple
type AppleIdAuth = {
auth: {
init(
config: {
clientId?: string;
scope?: string;
redirectURI?: string;
state?: string;
nonce?: string;
usePopup: boolean;
} & Record<string, unknown>
): void;
signIn(): Promise<Record<string, unknown>>;
};
};
declare const AppleID: AppleIdAuth | undefined;
export const getAppleSdk = () => {
if (AppleID === undefined) {
throw new Error('AppleID auth SDK not found.');
}
return AppleID;
};
const useAppleAuth = () => {
const navigate = useNavigate();
const auth = async (connectorId: string, redirectUri: string) => {
const url = new URL(redirectUri);
const { redirect_uri: redirectURI, ...rest } = parseQueryParameters(url.searchParams);
const config = {
redirectURI: redirectURI ?? '',
...camelcaseKeys(rest),
};
const { auth } = getAppleSdk();
auth.init({ usePopup: true, ...config });
try {
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
const data = await auth.signIn();
const { authorization } = data;
if (!authorization || typeof authorization !== 'object') {
throw new TypeError('Missing authorization object.');
}
const state = inOperator('state', authorization) ? String(authorization.state) : '';
const parameters = new URLSearchParams({
state,
// Due to the design limit of connectors, we have to use key `code`.
// TO-DO: @Darcy @Simeng update key after refactoring
code: JSON.stringify(data),
});
navigate(`/sign-in/callback/${connectorId}?${parameters.toString()}`);
} catch (error: unknown) {
// TO-DO: @Simeng handle error properly
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3523993
console.log('error!', error);
}
};
return auth;
};
export default useAppleAuth;

View file

@ -14,7 +14,8 @@ const useSocialCallbackHandler = () => {
const navigate = useNavigate();
const socialCallbackHandler = useCallback(() => {
const { state, error, error_description } = parseQueryParameters(window.location.search);
const data = window.location.search || '?' + window.location.hash.slice(1);
const { state, error, error_description } = parseQueryParameters(data);
const connectorId = parameters.connector;
// Connector auth error
@ -45,7 +46,7 @@ const useSocialCallbackHandler = () => {
navigate(
{
pathname: `/sign-in/callback/${connectorId}`,
search: window.location.search,
search: data,
},
{
replace: true,
@ -60,7 +61,7 @@ const useSocialCallbackHandler = () => {
throw new Error('CallbackLink is empty');
}
window.location.assign(new URL(`${callbackLink}${window.location.search}`));
window.location.assign(new URL(`${callbackLink}${data}`));
}, [navigate, parameters.connector, setToast, t]);
return socialCallbackHandler;

View file

@ -37,7 +37,7 @@ const useSocialSignInListener = () => {
);
const signInWithSocialHandler = useCallback(
async (connectorId: string, state: string, code: string) => {
async (connectorId: string, code: string) => {
void asyncSignInWithSocial({
connectorId,
code,
@ -59,7 +59,7 @@ const useSocialSignInListener = () => {
return;
}
const { state, code, ...rest } = parseQueryParameters(window.location.search);
const { state, ...rest } = parseQueryParameters(window.location.search);
if (!state || !stateValidation(state, parameters.connector)) {
setToast(t('error.invalid_connector_auth'));
@ -67,7 +67,7 @@ const useSocialSignInListener = () => {
return;
}
void signInWithSocialHandler(parameters.connector, state, code ?? JSON.stringify(rest));
void signInWithSocialHandler(parameters.connector, JSON.stringify(rest));
}, [parameters.connector, setToast, signInWithSocialHandler, t]);
};

View file

@ -3,7 +3,6 @@ import { useCallback, useContext } from 'react';
import { invokeSocialSignIn } from '@/apis/social';
import useApi from './use-api';
import useAppleAuth, { isAppleConnector } from './use-apple-auth';
import { PageContext } from './use-page-context';
import useTerms from './use-terms';
import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils';
@ -11,7 +10,6 @@ import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from '.
const useSocial = () => {
const { experienceSettings } = useContext(PageContext);
const { termsValidation } = useTerms();
const appleAuth = useAppleAuth();
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
@ -39,13 +37,6 @@ const useSocial = () => {
// Callback hook to close the social sign in modal
callback?.();
// For Sign In with Apple, use the official SDK directly
if (isAppleConnector({ target })) {
await appleAuth(connectorId, result.redirectTo);
return;
}
// Invoke Native Social Sign In flow
if (isNativeWebview()) {
getLogtoNativeSdk()?.getPostMessage()({

View file

@ -6,7 +6,6 @@
import { SignInMethods } from '@logto/schemas';
import { getSignInExperience } from '@/apis/settings';
import { isAppleConnector } from '@/hooks/use-apple-auth';
import { filterSocialConnectors } from '@/hooks/utils';
import { SignInMethod, SignInExperienceSettingsResponse, SignInExperienceSettings } from '@/types';
@ -29,14 +28,6 @@ export const getSecondarySignInMethods = (signInMethods: SignInMethods) =>
return methods;
}, []);
export const isAppleConnectorEnabled = ({
primarySignInMethod,
secondarySignInMethods,
socialConnectors,
}: SignInExperienceSettings) =>
(primarySignInMethod === 'social' || secondarySignInMethods.includes('social')) &&
socialConnectors.some((connector) => isAppleConnector(connector));
const getSignInExperienceSettings = async (): Promise<SignInExperienceSettings> => {
const { signInMethods, socialConnectors, ...rest } =
await getSignInExperience<SignInExperienceSettingsResponse>();