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:
commit
8d17f9d73a
10 changed files with 24 additions and 151 deletions
|
@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
||||
|
|
|
@ -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()({
|
||||
|
|
|
@ -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>();
|
||||
|
|
Loading…
Add table
Reference in a new issue