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 nock from 'nock';
|
||||||
|
|
||||||
import AppleConnector from '.';
|
import AppleConnector from '.';
|
||||||
import { defaultMetadata } from './constant';
|
import { authorizationEndpoint } from './constant';
|
||||||
import { mockedConfig } from './mock';
|
import { mockedConfig } from './mock';
|
||||||
import { AppleConfig } from './types';
|
import { AppleConfig } from './types';
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ describe('getAuthorizationUri', () => {
|
||||||
'http://localhost:3000/callback'
|
'http://localhost:3000/callback'
|
||||||
);
|
);
|
||||||
expect(authorizationUri).toEqual(
|
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,
|
SocialConnector,
|
||||||
GetConnectorConfig,
|
GetConnectorConfig,
|
||||||
} from '@logto/connector-types';
|
} from '@logto/connector-types';
|
||||||
import { conditional } from '@silverhand/essentials';
|
|
||||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
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';
|
import { appleConfigGuard, AppleConfig, appleDataGuard } from './types';
|
||||||
|
|
||||||
// TO-DO: support nonce validation
|
// TO-DO: support nonce validation
|
||||||
|
@ -37,9 +36,12 @@ export default class AppleConnector implements SocialConnector<string> {
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
scope,
|
scope,
|
||||||
state,
|
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.
|
// Directly return now. Refactor with connector interface redesign.
|
||||||
|
@ -50,10 +52,7 @@ export default class AppleConnector implements SocialConnector<string> {
|
||||||
// Extract data from JSON string.
|
// Extract data from JSON string.
|
||||||
// Refactor with connector interface redesign.
|
// Refactor with connector interface redesign.
|
||||||
public getUserInfo: GetUserInfo<string> = async (data) => {
|
public getUserInfo: GetUserInfo<string> = async (data) => {
|
||||||
const {
|
const { id_token: idToken } = appleDataGuard.parse(JSON.parse(data));
|
||||||
authorization: { id_token: idToken },
|
|
||||||
user,
|
|
||||||
} = appleDataGuard.parse(JSON.parse(data));
|
|
||||||
|
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||||
|
@ -71,13 +70,8 @@ export default class AppleConnector implements SocialConnector<string> {
|
||||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = [user?.name?.firstName, user?.name?.lastName]
|
|
||||||
.filter((value) => Boolean(value))
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: payload.sub,
|
id: payload.sub,
|
||||||
name: conditional(name),
|
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
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
|
// 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({
|
export const appleDataGuard = z.object({
|
||||||
authorization: z.object({
|
id_token: z.string(),
|
||||||
id_token: z.string(),
|
|
||||||
}),
|
|
||||||
user: z
|
|
||||||
.object({
|
|
||||||
name: z
|
|
||||||
.object({
|
|
||||||
firstName: z.string().optional(),
|
|
||||||
lastName: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppleData = z.infer<typeof appleDataGuard>;
|
export type AppleData = z.infer<typeof appleDataGuard>;
|
||||||
|
|
|
@ -40,8 +40,14 @@ export const getUserInfoByAuthCode = async (
|
||||||
authCode: string,
|
authCode: string,
|
||||||
redirectUri: string
|
redirectUri: string
|
||||||
): Promise<SocialUserInfo> => {
|
): 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 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);
|
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 * as styles from './App.module.scss';
|
||||||
import AppContent from './components/AppContent';
|
import AppContent from './components/AppContent';
|
||||||
import { loadAppleSdk } from './hooks/use-apple-auth';
|
|
||||||
import usePageContext from './hooks/use-page-context';
|
import usePageContext from './hooks/use-page-context';
|
||||||
import usePreview from './hooks/use-preview';
|
import usePreview from './hooks/use-preview';
|
||||||
import initI18n from './i18n/init';
|
import initI18n from './i18n/init';
|
||||||
|
@ -17,7 +16,7 @@ import SecondarySignIn from './pages/SecondarySignIn';
|
||||||
import SignIn from './pages/SignIn';
|
import SignIn from './pages/SignIn';
|
||||||
import SocialRegister from './pages/SocialRegister';
|
import SocialRegister from './pages/SocialRegister';
|
||||||
import SocialSignInCallback from './pages/SocialSignInCallback';
|
import SocialSignInCallback from './pages/SocialSignInCallback';
|
||||||
import getSignInExperienceSettings, { isAppleConnectorEnabled } from './utils/sign-in-experience';
|
import getSignInExperienceSettings from './utils/sign-in-experience';
|
||||||
|
|
||||||
import './scss/normalized.scss';
|
import './scss/normalized.scss';
|
||||||
|
|
||||||
|
@ -36,11 +35,6 @@ const App = () => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const settings = await getSignInExperienceSettings();
|
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
|
// Note: i18n must be initialized ahead of global experience settings
|
||||||
await initI18n(settings.languageInfo);
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
const socialCallbackHandler = useCallback(() => {
|
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;
|
const connectorId = parameters.connector;
|
||||||
|
|
||||||
// Connector auth error
|
// Connector auth error
|
||||||
|
@ -45,7 +46,7 @@ const useSocialCallbackHandler = () => {
|
||||||
navigate(
|
navigate(
|
||||||
{
|
{
|
||||||
pathname: `/sign-in/callback/${connectorId}`,
|
pathname: `/sign-in/callback/${connectorId}`,
|
||||||
search: window.location.search,
|
search: data,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replace: true,
|
replace: true,
|
||||||
|
@ -60,7 +61,7 @@ const useSocialCallbackHandler = () => {
|
||||||
throw new Error('CallbackLink is empty');
|
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]);
|
}, [navigate, parameters.connector, setToast, t]);
|
||||||
|
|
||||||
return socialCallbackHandler;
|
return socialCallbackHandler;
|
||||||
|
|
|
@ -37,7 +37,7 @@ const useSocialSignInListener = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const signInWithSocialHandler = useCallback(
|
const signInWithSocialHandler = useCallback(
|
||||||
async (connectorId: string, state: string, code: string) => {
|
async (connectorId: string, code: string) => {
|
||||||
void asyncSignInWithSocial({
|
void asyncSignInWithSocial({
|
||||||
connectorId,
|
connectorId,
|
||||||
code,
|
code,
|
||||||
|
@ -59,7 +59,7 @@ const useSocialSignInListener = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { state, code, ...rest } = parseQueryParameters(window.location.search);
|
const { state, ...rest } = parseQueryParameters(window.location.search);
|
||||||
|
|
||||||
if (!state || !stateValidation(state, parameters.connector)) {
|
if (!state || !stateValidation(state, parameters.connector)) {
|
||||||
setToast(t('error.invalid_connector_auth'));
|
setToast(t('error.invalid_connector_auth'));
|
||||||
|
@ -67,7 +67,7 @@ const useSocialSignInListener = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void signInWithSocialHandler(parameters.connector, state, code ?? JSON.stringify(rest));
|
void signInWithSocialHandler(parameters.connector, JSON.stringify(rest));
|
||||||
}, [parameters.connector, setToast, signInWithSocialHandler, t]);
|
}, [parameters.connector, setToast, signInWithSocialHandler, t]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useCallback, useContext } from 'react';
|
||||||
import { invokeSocialSignIn } from '@/apis/social';
|
import { invokeSocialSignIn } from '@/apis/social';
|
||||||
|
|
||||||
import useApi from './use-api';
|
import useApi from './use-api';
|
||||||
import useAppleAuth, { isAppleConnector } from './use-apple-auth';
|
|
||||||
import { PageContext } from './use-page-context';
|
import { PageContext } from './use-page-context';
|
||||||
import useTerms from './use-terms';
|
import useTerms from './use-terms';
|
||||||
import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils';
|
import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils';
|
||||||
|
@ -11,7 +10,6 @@ import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from '.
|
||||||
const useSocial = () => {
|
const useSocial = () => {
|
||||||
const { experienceSettings } = useContext(PageContext);
|
const { experienceSettings } = useContext(PageContext);
|
||||||
const { termsValidation } = useTerms();
|
const { termsValidation } = useTerms();
|
||||||
const appleAuth = useAppleAuth();
|
|
||||||
|
|
||||||
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
||||||
|
|
||||||
|
@ -39,13 +37,6 @@ const useSocial = () => {
|
||||||
// Callback hook to close the social sign in modal
|
// Callback hook to close the social sign in modal
|
||||||
callback?.();
|
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
|
// Invoke Native Social Sign In flow
|
||||||
if (isNativeWebview()) {
|
if (isNativeWebview()) {
|
||||||
getLogtoNativeSdk()?.getPostMessage()({
|
getLogtoNativeSdk()?.getPostMessage()({
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { SignInMethods } from '@logto/schemas';
|
import { SignInMethods } from '@logto/schemas';
|
||||||
|
|
||||||
import { getSignInExperience } from '@/apis/settings';
|
import { getSignInExperience } from '@/apis/settings';
|
||||||
import { isAppleConnector } from '@/hooks/use-apple-auth';
|
|
||||||
import { filterSocialConnectors } from '@/hooks/utils';
|
import { filterSocialConnectors } from '@/hooks/utils';
|
||||||
import { SignInMethod, SignInExperienceSettingsResponse, SignInExperienceSettings } from '@/types';
|
import { SignInMethod, SignInExperienceSettingsResponse, SignInExperienceSettings } from '@/types';
|
||||||
|
|
||||||
|
@ -29,14 +28,6 @@ export const getSecondarySignInMethods = (signInMethods: SignInMethods) =>
|
||||||
return methods;
|
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 getSignInExperienceSettings = async (): Promise<SignInExperienceSettings> => {
|
||||||
const { signInMethods, socialConnectors, ...rest } =
|
const { signInMethods, socialConnectors, ...rest } =
|
||||||
await getSignInExperience<SignInExperienceSettingsResponse>();
|
await getSignInExperience<SignInExperienceSettingsResponse>();
|
||||||
|
|
Loading…
Add table
Reference in a new issue