Skip to content

Commit

Permalink
feat: add Linkedin Oauth 2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien2p committed Nov 9, 2022
1 parent 1f1bdb4 commit acc036b
Show file tree
Hide file tree
Showing 15 changed files with 643 additions and 104 deletions.
117 changes: 54 additions & 63 deletions packages/medusa-plugin-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,9 @@ Then, in your medusa config plugins collection you can add the following configu
Now you can add your Google sign in button in your client with something along the line of the code bellow

```html
<a
type="button"
href=`${medusa_url}/${google_authPath}`
className="text-white bg-[#4285F4] hover:bg-[#4285F4]/90 focus:ring-4 focus:outline-none focus:ring-[#4285F4]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#4285F4]/55 mr-2 mb-2"
>
<svg
className="mr-2 -ml-1 w-4 h-4"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="google"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
>
<path
fill="currentColor"
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
></path>
</svg>
Sign in with Google
<a href="${medusa_url}/${google_authPath}" type="button" class="text-white bg-[#3b5998] hover:bg-[#3b5998]/90 focus:ring-4 focus:outline-none focus:ring-[#3b5998]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#3b5998]/55 mr-2 mb-2">
<svg class="mr-2 -ml-1 w-4 h-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="facebook-f" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M279.1 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.4 0 225.4 0c-73.22 0-121.1 44.38-121.1 124.7v70.62H22.89V288h81.39v224h100.2V288z"></path></svg>
Sign in with Facebook
</a>
```

Expand Down Expand Up @@ -142,26 +124,8 @@ Then, in your medusa config plugins collection you can add the following configu
Now you can add your Facebook sign in button in your client with something along the line of the code bellow

```html
<a
type="button"
href=`${medusa_url}/${facebook_authPath}`
className="text-white bg-[#3b5998] hover:bg-[#3b5998]/90 focus:ring-4 focus:outline-none focus:ring-[#3b5998]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#3b5998]/55 mr-2 mb-2"
>
<svg
className="mr-2 -ml-1 w-4 h-4"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="facebook-f"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
>
<path
fill="currentColor"
d="M279.1 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.4 0 225.4 0c-73.22 0-121.1 44.38-121.1 124.7v70.62H22.89V288h81.39v224h100.2V288z"
></path>
</svg>
<a href="${medusa_url}/${facebook_authPath}" type="button" class="text-white bg-[#3b5998] hover:bg-[#3b5998]/90 focus:ring-4 focus:outline-none focus:ring-[#3b5998]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#3b5998]/55 mr-2 mb-2">
<svg class="mr-2 -ml-1 w-4 h-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="facebook-f" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="currentColor" d="M279.1 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.4 0 225.4 0c-73.22 0-121.1 44.38-121.1 124.7v70.62H22.89V288h81.39v224h100.2V288z"></path></svg>
Sign in with Facebook
</a>
```
Expand Down Expand Up @@ -213,33 +177,60 @@ Then, in your medusa config plugins collection you can add the following configu
Now you can add your Twitter sign in button in your client with something along the line of the code bellow

```html
<a
type="button"
href=`${medusa_url}/${twitter_authPath}`
className="text-white bg-[#3b5998] hover:bg-[#3b5998]/90 focus:ring-4 focus:outline-none focus:ring-[#3b5998]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#3b5998]/55 mr-2 mb-2"
>
<svg
className="mr-2 -ml-1 w-4 h-4"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="twitter"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="M459.4 151.7c.325 4.548 .325 9.097 .325 13.65 0 138.7-105.6 298.6-298.6 298.6-59.45 0-114.7-17.22-161.1-47.11 8.447 .974 16.57 1.299 25.34 1.299 49.06 0 94.21-16.57 130.3-44.83-46.13-.975-84.79-31.19-98.11-72.77 6.498 .974 12.99 1.624 19.82 1.624 9.421 0 18.84-1.3 27.61-3.573-48.08-9.747-84.14-51.98-84.14-102.1v-1.299c13.97 7.797 30.21 12.67 47.43 13.32-28.26-18.84-46.78-51.01-46.78-87.39 0-19.49 5.197-37.36 14.29-52.95 51.65 63.67 129.3 105.3 216.4 109.8-1.624-7.797-2.599-15.92-2.599-24.04 0-57.83 46.78-104.9 104.9-104.9 30.21 0 57.5 12.67 76.67 33.14 23.72-4.548 46.46-13.32 66.6-25.34-7.798 24.37-24.37 44.83-46.13 57.83 21.12-2.273 41.58-8.122 60.43-16.24-14.29 20.79-32.16 39.31-52.63 54.25z"
></path>
</svg>
Sign in with Twitter
<a href="${medusa_url}/${twitter_authPath}" type="button" class="text-white bg-[#1da1f2] hover:bg-[#1da1f2]/90 focus:ring-4 focus:outline-none focus:ring-[#1da1f2]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#1da1f2]/55 mr-2 mb-2">
<svg class="mr-2 -ml-1 w-4 h-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="twitter" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M459.4 151.7c.325 4.548 .325 9.097 .325 13.65 0 138.7-105.6 298.6-298.6 298.6-59.45 0-114.7-17.22-161.1-47.11 8.447 .974 16.57 1.299 25.34 1.299 49.06 0 94.21-16.57 130.3-44.83-46.13-.975-84.79-31.19-98.11-72.77 6.498 .974 12.99 1.624 19.82 1.624 9.421 0 18.84-1.3 27.61-3.573-48.08-9.747-84.14-51.98-84.14-102.1v-1.299c13.97 7.797 30.21 12.67 47.43 13.32-28.26-18.84-46.78-51.01-46.78-87.39 0-19.49 5.197-37.36 14.29-52.95 51.65 63.67 129.3 105.3 216.4 109.8-1.624-7.797-2.599-15.92-2.599-24.04 0-57.83 46.78-104.9 104.9-104.9 30.21 0 57.5 12.67 76.67 33.14 23.72-4.548 46.46-13.32 66.6-25.34-7.798 24.37-24.37 44.83-46.13 57.83 21.12-2.273 41.58-8.122 60.43-16.24-14.29 20.79-32.16 39.31-52.63 54.25z"></path></svg>
Sign in with Twitter
</a>
```

### Linkedin

Coming soon
> 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 customised 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/linkedin/types.ts))

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

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

expiresIn: 24 * 60 * 60 * 1000
}
}
}
}
```

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

```html
<a href="${medusa_url}/${linkedin_authPath}" type="button" class="text-white bg-[#2663eb] hover:bg-[#2663eb]/90 focus:ring-4 focus:outline-none focus:ring-[#2663eb]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-[#2663eb]/55 mr-2 mb-2">
<svg class="mr-2 -ml-1 w-4 h-4" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="linkedin" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="currentColor" d="M218.123122,218.127392 L180.191928,218.127392 L180.191928,158.724263 C180.191928,144.559023 179.939053,126.323993 160.463756,126.323993 C140.707926,126.323993 137.685284,141.757585 137.685284,157.692986 L137.685284,218.123441 L99.7540894,218.123441 L99.7540894,95.9665207 L136.168036,95.9665207 L136.168036,112.660562 L136.677736,112.660562 C144.102746,99.9650027 157.908637,92.3824528 172.605689,92.9280076 C211.050535,92.9280076 218.138927,118.216023 218.138927,151.114151 L218.123122,218.127392 Z M56.9550587,79.2685282 C44.7981969,79.2707099 34.9413443,69.4171797 34.9391618,57.260052 C34.93698,45.1029244 44.7902948,35.2458562 56.9471566,35.2436736 C69.1040185,35.2414916 78.9608713,45.0950217 78.963054,57.2521493 C78.9641017,63.090208 76.6459976,68.6895714 72.5186979,72.8184433 C68.3913982,76.9473153 62.7929898,79.26748 56.9550587,79.2685282 M75.9206558,218.127392 L37.94995,218.127392 L37.94995,95.9665207 L75.9206558,95.9665207 L75.9206558,218.127392 Z M237.033403,0.0182577091 L18.8895249,0.0182577091 C8.57959469,-0.0980923971 0.124827038,8.16056231 -0.001,18.4706066 L-0.001,237.524091 C0.120519052,247.839103 8.57460631,256.105934 18.8895249,255.9977 L237.033403,255.9977 C247.368728,256.125818 255.855922,247.859464 255.999,237.524091 L255.999,18.4548016 C255.851624,8.12438979 247.363742,-0.133792868 237.033403,0.000790807055"></path></svg>
Sign in with Linkedin
</a>
```

### Github

Expand Down
3 changes: 2 additions & 1 deletion packages/medusa-plugin-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0"
"passport-google-oauth2": "^0.2.0",
"passport-linkedin-oauth2": "^2.0.0"
},
"jest": {
"preset": "ts-jest",
Expand Down
2 changes: 2 additions & 0 deletions packages/medusa-plugin-auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { loadJwtOverrideStrategy } from '../auth-strategies/jwt-override';
import { getGoogleRoutes } from '../auth-strategies/google';
import { getFacebookRoutes } from '../auth-strategies/facebook';
import { getTwitterRoutes } from '../auth-strategies/twitter';
import { getLinkedinRoutes } from "../auth-strategies/linkedin";

export default function (rootDirectory, pluginOptions: AuthOptions): Router[] {
const configModule = loadConfig(rootDirectory) as ConfigModule;
Expand All @@ -24,6 +25,7 @@ function loadRouters(configModule: ConfigModule, options: AuthOptions): Router[]
routers.push(...getGoogleRoutes(configModule, options));
routers.push(...getFacebookRoutes(configModule, options));
routers.push(...getTwitterRoutes(configModule, options));
routers.push(...getLinkedinRoutes(configModule, options));
routers.push(getLogoutRouter(configModule));

return routers;
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('Linkedin 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('Linkedin 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 acc036b

Please sign in to comment.