Skip to content

Commit

Permalink
Compatibility with Medusa 1.17 sessions (#99)
Browse files Browse the repository at this point in the history
* compatible with 1.17 sessions

* added support for bearer auth

* appending access token to url params

* added expires in functionality back

* fixed errors

* added docs

* resolved pr comments

* minor formatting
  • Loading branch information
dPreininger authored Nov 13, 2023
1 parent 78f55ca commit 92ff54e
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 86 deletions.
17 changes: 16 additions & 1 deletion docs/pages/authentication/auth0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ newly added plugins. To do so here are the steps

<CircleStep index={4}>
Update your client to add the authentication action

After authentication is successfull, the user will be redirected to the `successRedirect` url with a `?access_token=your_token` query param and a session cookie will be set. Session will automatically be authenticated. The access token is a JWT that can be used with Authorization: Bearer <token> header to authenticate requests to the backend instead of using a session cookie.
</CircleStep>

### Default behaviour
Expand All @@ -100,4 +102,17 @@ The default `verifyCallback` flow looks as follow (unless the `strict` option is
- In the case another external authentication method have been used in the past, then an unauthorized
will be returned.
- if the customer trying to authenticate does not exist, a new customer will be created and the authentication
flow follow the previous point
flow follow the previous point

### Request only access token in a JSON response

If you are using your Medusa application with a SPA or mobile frontend, you can request opt to receive the access token in a JSON response instead of a redirect.

To do this, you have to add ?returnAccessToken=true query parameter to your authentication call. Your authentication URL will then look like this: ${medusa_url}/${authPath}?returnAccessToken=true

The response will look like this:
```json
{
access_token: "<your-jwt-token>"
}
```
17 changes: 16 additions & 1 deletion docs/pages/authentication/azureoidc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ newly added plugins. To do so here are the steps
```
- `medusa_url` correspond to your backend server url (e.g `http://localhost:9000`)
- `authPath` correspond to `authPath` configuration in your plugin (e.g `admin/auth/azure`)

After authentication is successfull, the user will be redirected to the `successRedirect` url with a `?access_token=your_token` query param and a session cookie will be set. Session will automatically be authenticated. The access token is a JWT that can be used with Authorization: Bearer <token> header to authenticate requests to the backend instead of using a session cookie.
</CircleStep>

### Implementation Comments
Expand All @@ -131,4 +133,17 @@ The default `verifyCallback` flow looks as follow (unless the `strict` option is
- In the case another external authentication method have been used in the past, then an unauthorized
will be returned.
- if the customer trying to authenticate does not exist, a new customer will be created and the authentication
flow follow the previous point
flow follow the previous point

### Request only access token in a JSON response

If you are using your Medusa application with a SPA or mobile frontend, you can request opt to receive the access token in a JSON response instead of a redirect.

To do this, you have to add ?returnAccessToken=true query parameter to your authentication call. Your authentication URL will then look like this: ${medusa_url}/${authPath}?returnAccessToken=true

The response will look like this:
```json
{
access_token: "<your-jwt-token>"
}
```
17 changes: 16 additions & 1 deletion docs/pages/authentication/facebook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ newly added plugins. To do so here are the steps
```
- `medusa_url` correspond to your backend server url (e.g `http://localhost:9000`)
- `authPath` correspond to `authPath` configuration in your plugin (e.g `admin/auth/facebook`)

After authentication is successfull, the user will be redirected to the `successRedirect` url with a `?access_token=your_token` query param and a session cookie will be set. Session will automatically be authenticated. The access token is a JWT that can be used with Authorization: Bearer <token> header to authenticate requests to the backend instead of using a session cookie.
</CircleStep>

### Default behaviour
Expand All @@ -106,4 +108,17 @@ The default `verifyCallback` flow looks as follow (unless the `strict` option is
- In the case another external authentication method have been used in the past, then an unauthorized
will be returned.
- if the customer trying to authenticate does not exist, a new customer will be created and the authentication
flow follow the previous point
flow follow the previous point

### Request only access token in a JSON response

If you are using your Medusa application with a SPA or mobile frontend, you can request opt to receive the access token in a JSON response instead of a redirect.

To do this, you have to add ?returnAccessToken=true query parameter to your authentication call. Your authentication URL will then look like this: ${medusa_url}/${authPath}?returnAccessToken=true

The response will look like this:
```json
{
access_token: "<your-jwt-token>"
}
```
17 changes: 16 additions & 1 deletion docs/pages/authentication/google.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ newly added plugins. To do so here are the steps
```
- `medusa_url` correspond to your backend server url (e.g `http://localhost:9000`)
- `authPath` correspond to `authPath` configuration in your plugin (e.g `admin/auth/google`)

After authentication is successfull, the user will be redirected to the `successRedirect` url with a `?access_token=your_token` query param and a session cookie will be set. Session will automatically be authenticated. The access token is a JWT that can be used with Authorization: Bearer <token> header to authenticate requests to the backend instead of using a session cookie.
</CircleStep>

### Default behaviour
Expand All @@ -106,4 +108,17 @@ The default `verifyCallback` flow looks as follow (unless the `strict` option is
- In the case another external authentication method have been used in the past, then an unauthorized
will be returned.
- if the customer trying to authenticate does not exist, a new customer will be created and the authentication
flow follow the previous point
flow follow the previous point

### Request only access token in a JSON response

If you are using your Medusa application with a SPA or mobile frontend, you can request opt to receive the access token in a JSON response instead of a redirect.

To do this, you have to add ?returnAccessToken=true query parameter to your authentication call. Your authentication URL will then look like this: ${medusa_url}/${authPath}?returnAccessToken=true

The response will look like this:
```json
{
access_token: "<your-jwt-token>"
}
```
17 changes: 16 additions & 1 deletion docs/pages/authentication/linkedin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ newly added plugins. To do so here are the steps
```
- `medusa_url` correspond to your backend server url (e.g `http://localhost:9000`)
- `authPath` correspond to `authPath` configuration in your plugin (e.g `admin/auth/linkedin`)

After authentication is successfull, the user will be redirected to the `successRedirect` url with a `?access_token=your_token` query param and a session cookie will be set. Session will automatically be authenticated. The access token is a JWT that can be used with Authorization: Bearer <token> header to authenticate requests to the backend instead of using a session cookie.
</CircleStep>

### Default behaviour
Expand All @@ -106,4 +108,17 @@ The default `verifyCallback` flow looks as follow (unless the `strict` option is
- In the case another external authentication method have been used in the past, then an unauthorized
will be returned.
- if the customer trying to authenticate does not exist, a new customer will be created and the authentication
flow follow the previous point
flow follow the previous point

### Request only access token in a JSON response

If you are using your Medusa application with a SPA or mobile frontend, you can request opt to receive the access token in a JSON response instead of a redirect.

To do this, you have to add ?returnAccessToken=true query parameter to your authentication call. Your authentication URL will then look like this: ${medusa_url}/${authPath}?returnAccessToken=true

The response will look like this:
```json
{
access_token: "<your-jwt-token>"
}
```
4 changes: 2 additions & 2 deletions packages/medusa-plugin-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@
"test:ci": "yarn add -D @medusajs/medusa@${MEDUSAJS_VERSION} && yarn run test"
},
"peerDependencies": {
"@medusajs/medusa": "<=1.16.x",
"@medusajs/medusa": ">=1.16.x",
"passport": "^0.6.0",
"typeorm": "*"
},
"devDependencies": {
"@medusajs/medusa": "<=1.16.x",
"@medusajs/medusa": ">=1.17.x",
"@types/express": "^4.17.17",
"@types/jest": "^29.1.2",
"jest": "^29.1.2",
Expand Down
29 changes: 15 additions & 14 deletions packages/medusa-plugin-auth/src/auth-strategies/firebase/utils.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import passport from 'passport';
import cors from 'cors';
import { ConfigModule } from '@medusajs/medusa/dist/types/global';
import { Router } from 'express';
import { TWENTY_FOUR_HOURS_IN_MS } from '../../types';
import { sendTokenFactory } from '../../core/auth-callback-middleware';

function firebaseCallbackMiddleware(domain: 'admin' | 'store', secret: string, expiresIn: number) {
return (req, res) => {
const sendToken = sendTokenFactory(domain, secret, expiresIn);
sendToken(req, res);
res.status(200).json({ result: 'OK' });
import { Router, Request, Response } from 'express';
import { authenticateSessionFactory, signToken } from '../../core/auth-callback-middleware';

function firebaseCallbackMiddleware(domain: 'admin' | 'store', configModule: ConfigModule, expiresIn?: number) {
return (req: Request, res: Response) => {
if(req.query.returnAccessToken == 'true') {
const token = signToken(domain, configModule, req.user, expiresIn);
res.json({ access_token: token });
return;
} else {
authenticateSessionFactory(domain)(req, res);

res.status(200).json({ result: 'OK' });
}
};
}

Expand Down Expand Up @@ -40,11 +45,7 @@ export function firebaseAuthRoutesBuilder({
/*necessary if you are using non medusajs client such as a pure axios call, axios initially requests options and then get*/
router.options(authPath, cors(corsOptions));

const callbackHandler = firebaseCallbackMiddleware(
domain,
configModule.projectConfig.jwt_secret,
expiresIn ?? TWENTY_FOUR_HOURS_IN_MS
);
const callbackHandler = firebaseCallbackMiddleware(domain, configModule, expiresIn);

router.get(
authPath,
Expand Down
46 changes: 29 additions & 17 deletions packages/medusa-plugin-auth/src/core/auth-callback-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
import { Request, Response } from 'express';
import { ConfigModule } from '@medusajs/medusa/dist/types/global';
import jwt from 'jsonwebtoken';

/**
* Return the handler of the auth callback for an auth strategy. Once the auth is successful this callback
* will be called.
* @param domain
* @param secret
* @param expiresIn
* @param successRedirectGetter
* @param successAction
*/
export function authCallbackMiddleware(
domain: 'admin' | 'store',
secret: string,
expiresIn: number,
successRedirectGetter: () => string
successAction: (req: Request, res: Response) => void
) {
return (req, res) => {
const sendToken = sendTokenFactory(domain, secret, expiresIn);
sendToken(req, res);
res.redirect(successRedirectGetter());
successAction(req, res);
};
}

export function sendTokenFactory(domain: 'admin' | 'store', secret: string, expiresIn: number) {
export function signToken(domain: 'admin' | 'store', configModule: ConfigModule, user: any, expiresIn?: number) {
if(domain === 'admin') {
return jwt.sign(
{ user_id: user.id, domain: 'admin' },
configModule.projectConfig.jwt_secret,
{
expiresIn: expiresIn ?? '24h',
}
);
} else {
return jwt.sign(
{ customer_id: user.id, domain: 'store' },
configModule.projectConfig.jwt_secret,
{
expiresIn: expiresIn ?? '30d',
}
);
}
}

export function authenticateSessionFactory(domain: 'admin' | 'store') {
return (req, res) => {
const tokenData =
domain === 'admin' ? { userId: req.user.id, ...req.user } : { customer_id: req.user.id, ...req.user };
const token = jwt.sign(tokenData, secret, { expiresIn });
const sessionKey = domain === 'admin' ? 'jwt' : 'jwt_store';
req.session[sessionKey] = token;
const sessionKey = domain === 'admin' ? 'user_id' : 'customer_id';

req.session[sessionKey] = req.user.id;
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Router } from 'express';
import { Request, Response, Router } from 'express';
import passport from 'passport';
import cors from 'cors';
import { TWENTY_FOUR_HOURS_IN_MS } from '../../../types';
import { authCallbackMiddleware } from '../../auth-callback-middleware';
import { authCallbackMiddleware, authenticateSessionFactory, signToken } from '../../auth-callback-middleware';
import { ConfigModule } from '@medusajs/medusa/dist/types/global';

type PassportAuthenticateMiddlewareOptions = {
[key: string]: unknown;
scope?: unknown;
scope?: string | string[];
};

type PassportCallbackAuthenticateMiddlewareOptions = {
Expand All @@ -34,23 +33,24 @@ export function passportAuthRoutesBuilder({
strategyName,
passportAuthenticateMiddlewareOptions,
passportCallbackAuthenticateMiddlewareOptions,
expiresIn,
successRedirect,
authCallbackPath,
expiresIn,
}: {
domain: 'admin' | 'store';
configModule: ConfigModule;
authPath: string;
strategyName: string;
passportAuthenticateMiddlewareOptions: PassportAuthenticateMiddlewareOptions;
passportCallbackAuthenticateMiddlewareOptions: PassportCallbackAuthenticateMiddlewareOptions;
expiresIn?: number;
successRedirect: string;
authCallbackPath: string;
expiresIn?: number;
}): Router {
const router = Router();

const originalSuccessRedirect = successRedirect;
const defaultRedirect = successRedirect;
let successAction: (req: Request, res: Response) => void;

const corsOptions = {
origin:
Expand All @@ -67,7 +67,7 @@ export function passportAuthRoutesBuilder({
authPath,
(req, res, next) => {
// Allow to override the successRedirect by passing a query param `?redirectTo=your_url`
successRedirect = (req.query.redirectTo ? req.query.redirectTo : originalSuccessRedirect) as string;
successAction = successActionHandlerFactory(req, domain, configModule, defaultRedirect, expiresIn);
next();
},
passport.authenticate(strategyName, {
Expand All @@ -76,12 +76,7 @@ export function passportAuthRoutesBuilder({
})
);

const callbackHandler = authCallbackMiddleware(
domain,
configModule.projectConfig.jwt_secret,
expiresIn ?? TWENTY_FOUR_HOURS_IN_MS,
() => successRedirect
);
const callbackHandler = authCallbackMiddleware((req, res) => successAction(req, res));

router.get(authCallbackPath, cors(corsOptions));
router.get(
Expand Down Expand Up @@ -119,3 +114,29 @@ export function passportAuthRoutesBuilder({

return router;
}

function successActionHandlerFactory(req: Request, domain: 'admin' | 'store', configModule: ConfigModule, defaultRedirect: string, expiresIn?: number) {
const returnAccessToken = req.query.returnAccessToken == 'true';
const redirectUrl = (req.query.redirectTo ? req.query.redirectTo : defaultRedirect) as string;

if (returnAccessToken) {
return (req: Request, res: Response) => {
const token = signToken(domain, configModule, req.user, expiresIn);
res.json({ access_token: token });
};
}

return (req: Request, res: Response) => {
const authenticateSession = authenticateSessionFactory(domain);
authenticateSession(req, res);

const token = signToken(domain, configModule, req.user, expiresIn);


// append token to redirect url as query param
const url = new URL(redirectUrl);
url.searchParams.append('access_token', token);

res.redirect(url.toString());
};
}
Loading

0 comments on commit 92ff54e

Please sign in to comment.