|
|
@@ -155,6 +155,172 @@ export class GoogleAuthenticationStrategy implements AuthenticationStrategy<Goog
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+## Example: Facebook authentication
|
|
|
+
|
|
|
+This example demonstrates how to implement a Facebook login flow.
|
|
|
+
|
|
|
+### Storefront setup
|
|
|
+
|
|
|
+In this example we are assuming the use of the [Facebook SDK for JavaScript](https://developers.facebook.com/docs/javascript/) in the storefront.
|
|
|
+
|
|
|
+An implementation in React might look like this:
|
|
|
+
|
|
|
+```tsx
|
|
|
+/**
|
|
|
+ * Renders a Facebook login button.
|
|
|
+ */
|
|
|
+export const FBLoginButton = () => {
|
|
|
+ const fnName = `onFbLoginButtonSuccess`;
|
|
|
+ const router = useRouter();
|
|
|
+ const [error, setError] = useState('');
|
|
|
+ const [socialLoginMutation] = useMutation(AuthenticateDocument);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ (window as any)[fnName] = function () {
|
|
|
+ FB.getLoginStatus(login);
|
|
|
+ };
|
|
|
+ return () => {
|
|
|
+ delete (window as any)[fnName];
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ window?.FB?.XFBML.parse();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const login = async (response: any) => {
|
|
|
+ const { status, authResponse } = response;
|
|
|
+ if (status === 'connected') {
|
|
|
+ const result = await socialLoginMutation({ variables: { token: authResponse.accessToken } });
|
|
|
+ if (result.data?.authenticate.__typename === 'CurrentUser') {
|
|
|
+ // The user has logged in, refresh the browser
|
|
|
+ trackLogin('facebook');
|
|
|
+ router.reload();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ setError('An error occurred!');
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="text-center" style={{ width: 188, height: 28 }}>
|
|
|
+ <FacebookSDK />
|
|
|
+ <div
|
|
|
+ className="fb-login-button"
|
|
|
+ data-width=""
|
|
|
+ data-size="medium"
|
|
|
+ data-button-type="login_with"
|
|
|
+ data-layout="default"
|
|
|
+ data-auto-logout-link="false"
|
|
|
+ data-use-continue-as="false"
|
|
|
+ data-scope="public_profile,email"
|
|
|
+ data-onlogin={`${fnName}();`}
|
|
|
+ />
|
|
|
+ {error && <div className="text-sm text-red-500">{error}</div>}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+```TypeScript
|
|
|
+import {
|
|
|
+ AuthenticationStrategy,
|
|
|
+ ExternalAuthenticationService,
|
|
|
+ Injector,
|
|
|
+ Logger,
|
|
|
+ RequestContext,
|
|
|
+ User,
|
|
|
+ UserService,
|
|
|
+} from '@vendure/core';
|
|
|
+
|
|
|
+import { DocumentNode } from 'graphql';
|
|
|
+import gql from 'graphql-tag';
|
|
|
+import fetch from 'node-fetch';
|
|
|
+
|
|
|
+export type FacebookAuthData = {
|
|
|
+ token: string;
|
|
|
+};
|
|
|
+
|
|
|
+export type FacebookAuthConfig = {
|
|
|
+ appId: string;
|
|
|
+ appSecret: string;
|
|
|
+ clientToken: string;
|
|
|
+};
|
|
|
+
|
|
|
+export class FacebookAuthenticationStrategy implements AuthenticationStrategy<FacebookAuthData> {
|
|
|
+ readonly name = 'facebook';
|
|
|
+ private externalAuthenticationService: ExternalAuthenticationService;
|
|
|
+ private userService: UserService;
|
|
|
+
|
|
|
+ constructor(private config: FacebookAuthConfig) {}
|
|
|
+
|
|
|
+ init(injector: Injector) {
|
|
|
+ // The ExternalAuthenticationService is a helper service which encapsulates much
|
|
|
+ // of the common functionality related to dealing with external authentication
|
|
|
+ // providers.
|
|
|
+ this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
|
|
|
+ this.userService = injector.get(UserService);
|
|
|
+ }
|
|
|
+
|
|
|
+ defineInputType(): DocumentNode {
|
|
|
+ // Here we define the expected input object expected by the `authenticate` mutation
|
|
|
+ // under the "google" key.
|
|
|
+ return gql`
|
|
|
+ input FacebookAuthInput {
|
|
|
+ token: String!
|
|
|
+ }
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async getAppAccessToken() {
|
|
|
+ const resp = await fetch(
|
|
|
+ `https://graph.facebook.com/oauth/access_token?client_id=${this.config.appId}&client_secret=${this.config.appSecret}&grant_type=client_credentials`,
|
|
|
+ );
|
|
|
+ return await resp.json();
|
|
|
+ }
|
|
|
+
|
|
|
+ async authenticate(ctx: RequestContext, data: FacebookAuthData): Promise<User | false> {
|
|
|
+ const { token } = data;
|
|
|
+ const { access_token } = await this.getAppAccessToken();
|
|
|
+ const resp = await fetch(
|
|
|
+ `https://graph.facebook.com/debug_token?input_token=${token}&access_token=${access_token}`,
|
|
|
+ );
|
|
|
+ const result = await resp.json();
|
|
|
+
|
|
|
+ if (!result.data) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const uresp = await fetch(`https://graph.facebook.com/me?access_token=${token}&fields=email,first_name,last_name`);
|
|
|
+ const uresult = (await uresp.json()) as { id?: string; email: string; first_name: string; last_name: string };
|
|
|
+
|
|
|
+ if (!uresult.id) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const existingUser = await this.externalAuthenticationService.findCustomerUser(ctx, this.name, uresult.id);
|
|
|
+
|
|
|
+ if (existingUser) {
|
|
|
+ // This will select all the auth methods
|
|
|
+ return (await this.userService.getUserById(ctx, existingUser.id))!;
|
|
|
+ }
|
|
|
+
|
|
|
+ Logger.info(`User Create: ${JSON.stringify(uresult)}`);
|
|
|
+ const user = await this.externalAuthenticationService.createCustomerAndUser(ctx, {
|
|
|
+ strategy: this.name,
|
|
|
+ externalIdentifier: uresult.id,
|
|
|
+ verified: true,
|
|
|
+ emailAddress: uresult.email,
|
|
|
+ firstName: uresult.first_name,
|
|
|
+ lastName: uresult.last_name,
|
|
|
+ });
|
|
|
+
|
|
|
+ user.verified = true;
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
## Example: Keycloak authentication
|
|
|
|
|
|
Here's an example of an AuthenticationStrategy intended to be used on the Admin API. The use-case is when the company has an existing identity server for employees, and you'd like your Vendure shop admins to be able to authenticate with their existing accounts.
|