Skip to content

Commit

Permalink
feat: supposrt facebook authentication strategy (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien2p authored Nov 8, 2022
1 parent 48af15a commit 1dc7582
Show file tree
Hide file tree
Showing 17 changed files with 761 additions and 164 deletions.
123 changes: 65 additions & 58 deletions packages/medusa-plugin-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ You need to set up your Google OAuth 2 credentials and content screen in your de
> **Tip**: Do not forget to set the `Authorised JavaScript origins` and `Authorised redirect URIs` with your localhost:port domain in your
> **credentials OAuth2 Client ID** in your developer console when you work locally.
> By default, the admin only allow to authenticate while the store create a new user of it does not exists yet.
> By default, the admin only allow to authenticate while the store create a new user of it does not exist yet.
> This behaviour can be changed and customise by specifying a custom verifyCallback in the configuration.
Then, in your medusa config plugins collection you can add the following configuration and update it according to your requirements
Then, in your medusa config plugins collection you can add the following configuration and update it according to your requirements ([full configuration here](https://github.com/adrien2p/medusa-plugins/tree/main/packages/medusa-plugin-auth/src/auth-strategies/google/types.ts))

```ts
{
Expand Down Expand Up @@ -77,61 +77,7 @@ Then, in your medusa config plugins collection you can add the following configu
}
```

Here is the full configuration types

```typescript

export type AuthOptions = {
// ...
google?: {
clientID: string;
clientSecret: string;
admin?: {
callbackUrl: string;
successRedirect: string;
failureRedirect: string;
authPath: string;
authCallbackPath: string;
/**
* The default verify callback function will be used if this configuration is not specified
*/
verifyCallback?: (
container: MedusaContainer,
req: Request,
accessToken: string,
refreshToken: string,
profile: { emails: { value: string }[]; name?: { givenName?: string; familyName?: string } },
done: (err: null | unknown, data: null | { id: string }) => void
) => Promise<void>;

expiresIn?: string;
};
store?: {
callbackUrl: string;
successRedirect: string;
failureRedirect: string;
authPath: string;
authCallbackPath: string;
/**
* The default verify callback function will be used if this configuration is not specified
*/
verifyCallback?: (
container: MedusaContainer,
req: Request,
accessToken: string,
refreshToken: string,
profile: { emails: { value: string }[]; name?: { givenName?: string; familyName?: string } },
done: (err: null | unknown, data: null | { id: string }) => void
) => Promise<void>;

expiresIn?: string;
};
};
};

```

Now you can add your Google sign in button in your client with something along the lime of the code bellow
Now you can add your Google sign in button in your client with something along the line of the code bellow

```html
<a href=`${medusa_url}/${google_authPath}` aria-label="Continue with google" role="button" class="focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-gray-700 py-3.5 px-4 border rounded-lg border-gray-700 flex items-center w-full mt-10">
Expand All @@ -142,7 +88,68 @@ Now you can add your Google sign in button in your client with something along t

### Facebook

Coming soon
You need to set up your Facebook OAuth 2 in your developer console. You can follow the steps that are here https://help.vtex.com/tutorial/adding-a-client-id-and-a-client-secret-to-log-in-with-facebook--3R7rzXWG1GswWOIkYyy8SO

> By default, the admin only allow to authenticate while the store create a new user of it does not exist yet.
> This behaviour can be changed and customise by specifying a custom verifyCallback in the configuration.
Then, in your medusa config plugins collection you can add the following configuration and update it according to your requirements ([full configuration here](https://github.com/adrien2p/medusa-plugins/tree/main/packages/medusa-plugin-auth/src/auth-strategies/facebook/types.ts))

```ts
{
resolve: "medusa-plugin-auth",
options: {
// Enable facebook OAuth 2
facebook: {
clientID: "__YOUR_CLIENT_ID__",
clientSecret: "__YOUR_CLIENT_SECRET__",
// Enable facebook OAuth 2 for the admin domain
admin: {
callbackUrl:`${process.env.BACKEND_URL}/admin/auth/facebook/cb`,
failureRedirect: `${process.env.ADMIN_URL}/login`,
successRedirect: `${process.env.ADMIN_URL}/`,
authPath: "/admin/auth/facebook",
authCallbackPath: "/admin/auth/facebook/cb",

expiresIn: "24h"
},
// Enable facebook OAuth 2 for the store domain
store: {
callbackUrl:`${process.env.BACKEND_URL}/store/auth/facebook/cb`,
failureRedirect: `${process.env.STORE_URL}/login`,
successRedirect: `${process.env.STORE_URL}/`,
authPath: "/store/auth/facebook",
authCallbackPath: "/store/auth/facebook/cb",

expiresIn: "30d"
}
}
}
}
```

Now you can add your Facebook sign in button in your client with something along the line of the code bellow

```html
<a
href={"http://localhost:9000/admin/auth/facebook"}
className="flex items-center justify-center w-full px-4 py-2 mt-2 space-x-3 text-sm text-center bg-blue-500 text-white transition-colors duration-200 transform border rounded-lg dark:text-gray-300 dark:border-gray-300 hover:bg-gray-600 dark:hover:bg-gray-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
className="bi bi-facebook"
viewBox="0 0 16 16"
>
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z" />
</svg>
<span className="text-sm text-white dark:text-gray-200">
Sign in with facebook
</span>
</a>
```

### Twitter

Expand Down
1 change: 1 addition & 0 deletions packages/medusa-plugin-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"cors": "^2.8.5",
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0"
},
"jest": {
Expand Down
19 changes: 5 additions & 14 deletions packages/medusa-plugin-auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Router } from 'express';
import { ConfigModule } from '@medusajs/medusa/dist/types/global';
import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware';
import loadConfig from '@medusajs/medusa/dist/loaders/config';
import cors from 'cors';

import { AUTH_TOKEN_COOKIE_NAME, AuthOptions } from '../types';
import { loadJwtOverrideStrategy } from '../auth-strategies/jwt-override';
import { getGoogleAdminAuthRouter, getGoogleStoreAuthRouter } from '../auth-strategies/google';
import cors from 'cors';
import { getGoogleRoutes } from '../auth-strategies/google';
import { getFacebookRoutes } from '../auth-strategies/facebook';

export default function (rootDirectory, pluginOptions: AuthOptions): Router[] {
const configModule = loadConfig(rootDirectory) as ConfigModule;
Expand All @@ -19,18 +20,8 @@ export default function (rootDirectory, pluginOptions: AuthOptions): Router[] {
function loadRouters(configModule: ConfigModule, options: AuthOptions): Router[] {
const routers: Router[] = [];

const { google } = options;

if (google) {
if (google.admin) {
const router = getGoogleAdminAuthRouter(google, configModule);
routers.push(router);
}
if (google.store) {
const router = getGoogleStoreAuthRouter(google, configModule);
routers.push(router);
}
}
routers.push(...getGoogleRoutes(configModule, options));
routers.push(...getFacebookRoutes(configModule, options));

return [...routers, getLogoutRouter(configModule)];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { verifyAdminCallback } from '../../admin';
import { MedusaContainer } from '@medusajs/medusa/dist/types/global';

describe('Facebook admin strategy verify callback', function () {
const existsEmail = 'exists@test.fr';

let container: MedusaContainer;
let req: Request;
let accessToken: string;
let refreshToken: string;
let profile: { emails: { value: string }[]; name?: { givenName?: string; familyName?: string } };

beforeEach(() => {
profile = {
emails: [{ value: existsEmail }],
};

container = {
resolve: (name: string) => {
const container_ = {
userService: {
retrieveByEmail: jest.fn().mockImplementation(async (email: string) => {
if (email === existsEmail) {
return {
id: 'test',
};
}

return;
}),
},
};

return container_[name];
},
} as MedusaContainer;
});

afterEach(() => {
jest.clearAllMocks();
});

it('should success', async () => {
profile = {
emails: [{ value: existsEmail }],
};

const done = (err, data) => {
expect(data).toEqual(
expect.objectContaining({
id: 'test',
})
);
};

await verifyAdminCallback(container, req, accessToken, refreshToken, profile, done);
});

it('should fail if the user does not exists', async () => {
profile = {
emails: [{ value: 'fake' }],
};

const done = (err) => {
expect(err).toEqual(new Error(`Unable to authenticate the user with the email fake`));
};

await verifyAdminCallback(container, req, accessToken, refreshToken, profile, done);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { verifyStoreCallback } from '../../store';
import { MedusaContainer } from '@medusajs/medusa/dist/types/global';
import { ENTITY_METADATA_KEY } from '../../index';

describe('Facebook store strategy verify callback', function () {
const existsEmail = 'exists@test.fr';
const existsEmailWithMeta = 'exist2s@test.fr';

let container: MedusaContainer;
let req: Request;
let accessToken: string;
let refreshToken: string;
let profile: { emails: { value: string }[]; name?: { givenName?: string; familyName?: string } };

beforeEach(() => {
profile = {
emails: [{ value: existsEmail }],
};

container = {
resolve: <T>(name: string): T => {
const container_ = {
manager: {
transaction: function (cb) {
cb();
},
},
customerService: {
withTransaction: function () {
return this;
},
create: jest.fn().mockImplementation(async () => {
return { id: 'test' };
}),
retrieveByEmail: jest.fn().mockImplementation(async (email: string) => {
if (email === existsEmail) {
return {
id: 'test',
};
}

if (email === existsEmailWithMeta) {
return {
id: 'test2',
metadata: {
[ENTITY_METADATA_KEY]: true,
},
};
}

return;
}),
},
};

return container_[name];
},
} as MedusaContainer;
});

afterEach(() => {
jest.clearAllMocks();
});

it('should succeed', async () => {
profile = {
emails: [{ value: existsEmailWithMeta }],
};

const done = (err, data) => {
expect(data).toEqual(
expect.objectContaining({
id: 'test2',
})
);
};

await verifyStoreCallback(container, req, accessToken, refreshToken, profile, done);
});

it('should fail when the customer exists without the metadata', async () => {
profile = {
emails: [{ value: existsEmail }],
};

const done = (err) => {
expect(err).toEqual(new Error(`Customer with email ${existsEmail} already exists`));
};

await verifyStoreCallback(container, req, accessToken, refreshToken, profile, done);
});

it('should succeed and create a new customer if it has not been found', async () => {
profile = {
emails: [{ value: 'fake' }],
name: {
givenName: 'test',
familyName: 'test',
},
};

const done = (err, data) => {
expect(data).toEqual(
expect.objectContaining({
id: 'test',
})
);
};

await verifyStoreCallback(container, req, accessToken, refreshToken, profile, done);
});
});
Loading

0 comments on commit 1dc7582

Please sign in to comment.