From 52c745d41f8438b2a1f0ad6fb86f3a85b0c13fa3 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 27 May 2022 19:06:06 +0800 Subject: [PATCH 1/2] refactor(connector): remove apple js sdk --- packages/connector-apple/src/index.ts | 18 ++-- packages/connector-apple/src/types.ts | 14 +-- packages/core/src/lib/social.ts | 8 +- packages/ui/src/App.tsx | 8 +- packages/ui/src/hooks/use-apple-auth.ts | 92 ------------------- .../src/hooks/use-social-callback-handler.ts | 7 +- .../src/hooks/use-social-sign-in-listener.ts | 6 +- packages/ui/src/hooks/use-social.ts | 9 -- packages/ui/src/utils/sign-in-experience.ts | 9 -- 9 files changed, 22 insertions(+), 149 deletions(-) delete mode 100644 packages/ui/src/hooks/use-apple-auth.ts diff --git a/packages/connector-apple/src/index.ts b/packages/connector-apple/src/index.ts index 07569dfbf..3bb2731f0 100644 --- a/packages/connector-apple/src/index.ts +++ b/packages/connector-apple/src/index.ts @@ -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 { 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 { // Extract data from JSON string. // Refactor with connector interface redesign. public getUserInfo: GetUserInfo = 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 { 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); diff --git a/packages/connector-apple/src/types.ts b/packages/connector-apple/src/types.ts index b45d56480..c07aacb90 100644 --- a/packages/connector-apple/src/types.ts +++ b/packages/connector-apple/src/types.ts @@ -8,19 +8,7 @@ export type AppleConfig = z.infer; // 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; diff --git a/packages/core/src/lib/social.ts b/packages/core/src/lib/social.ts index 6c2432a4d..bccb02eb2 100644 --- a/packages/core/src/lib/social.ts +++ b/packages/core/src/lib/social.ts @@ -40,8 +40,14 @@ export const getUserInfoByAuthCode = async ( authCode: string, redirectUri: string ): Promise => { + // 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); }; diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 8953e6901..60a846e40 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -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); diff --git a/packages/ui/src/hooks/use-apple-auth.ts b/packages/ui/src/hooks/use-apple-auth.ts deleted file mode 100644 index 0c6ec9561..000000000 --- a/packages/ui/src/hooks/use-apple-auth.ts +++ /dev/null @@ -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 - ): void; - signIn(): Promise>; - }; -}; - -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; diff --git a/packages/ui/src/hooks/use-social-callback-handler.ts b/packages/ui/src/hooks/use-social-callback-handler.ts index b94430f70..c96f4d509 100644 --- a/packages/ui/src/hooks/use-social-callback-handler.ts +++ b/packages/ui/src/hooks/use-social-callback-handler.ts @@ -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; diff --git a/packages/ui/src/hooks/use-social-sign-in-listener.ts b/packages/ui/src/hooks/use-social-sign-in-listener.ts index 6b7afc2d8..c5dd48aa2 100644 --- a/packages/ui/src/hooks/use-social-sign-in-listener.ts +++ b/packages/ui/src/hooks/use-social-sign-in-listener.ts @@ -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]); }; diff --git a/packages/ui/src/hooks/use-social.ts b/packages/ui/src/hooks/use-social.ts index 910378ed7..a55a53fe5 100644 --- a/packages/ui/src/hooks/use-social.ts +++ b/packages/ui/src/hooks/use-social.ts @@ -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()({ diff --git a/packages/ui/src/utils/sign-in-experience.ts b/packages/ui/src/utils/sign-in-experience.ts index a978ed50a..2c866193e 100644 --- a/packages/ui/src/utils/sign-in-experience.ts +++ b/packages/ui/src/utils/sign-in-experience.ts @@ -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 => { const { signInMethods, socialConnectors, ...rest } = await getSignInExperience(); From d327c6fdf5f4a3fbc68618f46df7ac213d77aed5 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Fri, 27 May 2022 19:13:33 +0800 Subject: [PATCH 2/2] fix(connector): test --- packages/connector-apple/src/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connector-apple/src/index.test.ts b/packages/connector-apple/src/index.test.ts index 07958d618..10b850c7e 100644 --- a/packages/connector-apple/src/index.test.ts +++ b/packages/connector-apple/src/index.test.ts @@ -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` ); }); });