Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---
## [v1.0.0] - 2024-06-12
### Breaking changes
- Session cookie support for extension launch has been removed. This functionality is replaced with a temporary authorization token and a JWT (JSON Web Token) flow.
### Added
- Added a new routes `/fp/session_token` and `/adm/session_token` which can be used to get session token in exchange of temporary token. This session token will be used by Fynd Platform extensions to fetch data from fynd platform.
- These changes are applicable to both platform and admin panel extension launching flow.
---
## [v0.7.0] - 2024-02-02
### Added
- Added `partnerApiRoutes` to support launching of extension admin panel insie the partners panel.
- Added `partnerApiRoutes` to support launching of extension admin panel inside the partners panel.
- Added `PartnerClient` which can be used for calling partners server API
- Added support of passing log level `debug` to SDK from `setupFDK` debug true. This enables curl printing of API calls made from SDK.
---
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ FDK Extension Helper Library
```javascript
const bodyParser = require("body-parser");
const express = require("express");
const cookieParser = require("cookie-parser");
const { setupFdk } = require("fdk-extension-javascript/express");
const { RedisStorage } = require("fdk-extension-javascript/express/storage");
const Redis = require("ioredis");

const app = express();
app.use(cookieParser("ext.session"));
app.use(bodyParser.json({ limit: "2mb" }));

const redis = new Redis();
Expand Down Expand Up @@ -42,6 +40,15 @@ app.use(fdkClient.fdkHandler);
app.listen(8080);
```

#### How to fetch session(JWT) token?

After launching extension, you'll receive a temporary token included as a query params within the provided auth callback URL. This temporary token acts like a one-time key and expires within 30 seconds.
example: `<EXTENSION_BASE_URL>/company/:company_id?token=<TEMP_TOKEN>`

To establish a secure session, you'll need to exchange this temporary token for a longer-lasting session token (represented as a JWT or JSON Web Token). This exchange process involves sending a request to your application's backend API `/fp/session_token`. Include the temporary token recieved from the callback URL(`<EXTENSION_BASE_URL>/company/:company_id?token=<TEMP_TOKEN>`) within the request's authorization header.

If successful, the backend responds with the actual session token (JWT) that you can then store securely on the frontend for future authenticated requests. Remember, this session token also has an expiry, so to maintain access after it expires, you'll need to repeat the process of acquiring a new temporary token and exchanging it for a fresh session token.

#### How to call platform apis?

To call platform api you need to have instance of `PlatformClient`. Instance holds methods for SDK classes. All routes registered under `platformApiRoutes` express router will have `platformClient` under request object which is instance of `PlatformClient`.
Expand Down
4 changes: 2 additions & 2 deletions express/api_routes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';
const { extension } = require('./extension');
const express = require('express');
const { sessionMiddleware, partnerSessionMiddleware } = require('./middleware/session_middleware');
const { sessionMiddleware } = require('./middleware/session_middleware');
const { ApplicationConfig, ApplicationClient } = require("@gofynd/fdk-client-javascript");


Expand Down Expand Up @@ -43,7 +43,7 @@ function setupProxyRoutes(configData) {
}
});

partnerApiRoutes.use(partnerSessionMiddleware(true), async (req, res, next) => {
partnerApiRoutes.use(sessionMiddleware(true), async (req, res, next) => {
try {
const client = await extension.getPartnerClient(req.fdkSession.organization_id, req.fdkSession);
req.partnerClient = client;
Expand Down
29 changes: 5 additions & 24 deletions express/middleware/session_middleware.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
'use strict';
const { SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME } = require('./../constants');
const SessionStorage = require("../session/session_storage");

function sessionMiddleware(strict) {
return async (req, res, next) => {
try {
const companyId = req.headers['x-company-id'] || req.query['company_id'];
const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}`
let sessionId = req.signedCookies[compCookieName];
req.fdkSession = await SessionStorage.getSession(sessionId);
let token = req.query.token || req.headers.authorization;
if(!token)
return res.status(401).json({ "message": "Authorization token missing" });
req.fdkSession = await SessionStorage.getSession(token);

if(strict && !req.fdkSession) {
return res.status(401).json({ "message": "unauthorized" });
Expand All @@ -20,25 +19,7 @@ function sessionMiddleware(strict) {
};
}

function partnerSessionMiddleware(isStrict) {
return async (req, res, next) => {
try {
let sessionId = req.signedCookies[ADMIN_SESSION_COOKIE_NAME];
req.fdkSession = await SessionStorage.getSession(sessionId);

if (isStrict && !req.fdkSession) {
return res.status(401).json({"message": "Unauthorized"});
}
next();

} catch(error) {
next(error);
}
}
}


module.exports = {
sessionMiddleware : sessionMiddleware,
partnerSessionMiddleware: partnerSessionMiddleware
sessionMiddleware : sessionMiddleware
};
124 changes: 51 additions & 73 deletions express/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ const { v4: uuidv4 } = require("uuid");
const Session = require("./session/session");
const SessionStorage = require("./session/session_storage");
const { FdkSessionNotFoundError, FdkInvalidOAuthError } = require("./error_code");
const { SESSION_COOKIE_NAME, ADMIN_SESSION_COOKIE_NAME } = require('./constants');
const { sessionMiddleware, partnerSessionMiddleware } = require('./middleware/session_middleware');
const { sessionMiddleware } = require('./middleware/session_middleware');
const logger = require('./logger');
const urljoin = require('url-join');
const FdkRoutes = express.Router();
const jwt = require('jsonwebtoken');


function setupRoutes(ext) {
Expand All @@ -21,55 +21,45 @@ function setupRoutes(ext) {
try {
let companyId = parseInt(req.query.company_id);
let platformConfig = await ext.getPlatformConfig(companyId);

const state = uuidv4();
const payload = {
state: state,
companyId: companyId
}
const token = jwt.sign(payload, ext.api_secret);

let session;

session = new Session(Session.generateSessionId(true));

let sessionExpires = new Date(Date.now() + 900000); // 15 min

session = new Session(token);
if (session.isNew) {
session.company_id = companyId;
session.scope = ext.scopes;
session.expires = sessionExpires;
session.access_mode = 'online'; // Always generate online mode token for extension launch
session.extension_id = ext.api_key;
} else {
if (session.expires) {
session.expires = new Date(session.expires);
}
}
session.state = state;

req.fdkSession = session;
req.extension = ext;

const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}`
res.header['x-company-id'] = companyId;
res.cookie(compCookieName, session.id, {
secure: true,
httpOnly: true,
expires: session.expires,
signed: true,
sameSite: "None"
});

let redirectUrl;

session.state = uuidv4();

// pass application id if received
let authCallback = ext.getAuthCallback();
if (req.query.application_id) {
authCallback += "?application_id=" + req.query.application_id;
}

// start authorization flow
redirectUrl = platformConfig.oauthClient.startAuthorization({
let redirectUrl = platformConfig.oauthClient.startAuthorization({
scope: session.scope,
redirectUri: authCallback,
redirectUri: `${authCallback}?token=${token}`,
state: session.state,
access_mode: 'online' // Always generate online mode token for extension launch
});
await SessionStorage.saveSession(session);
await SessionStorage.saveSession(session, true);
logger.debug(`Redirecting after install callback to url: ${redirectUrl}`);
res.redirect(redirectUrl);
} catch (error) {
Expand All @@ -83,8 +73,7 @@ function setupRoutes(ext) {
if (!req.fdkSession) {
throw new FdkSessionNotFoundError("Can not complete oauth process as session not found");
}

if (req.fdkSession.state !== req.query.state) {
if (req.fdkSession.state !== req.query.state || req.fdkSession.company_id !== parseInt(req.query.company_id)) {
throw new FdkInvalidOAuthError("Invalid oauth call");
}
const companyId = req.fdkSession.company_id
Expand All @@ -99,7 +88,7 @@ function setupRoutes(ext) {
token.access_token_validity = sessionExpires.getTime();
req.fdkSession.updateToken(token);

await SessionStorage.saveSession(req.fdkSession);
await SessionStorage.saveSession(req.fdkSession, true);

// Generate separate access token for offline mode
if (!ext.isOnlineAccessMode()) {
Expand Down Expand Up @@ -128,16 +117,6 @@ function setupRoutes(ext) {
await SessionStorage.saveSession(session);

}

const compCookieName = `${SESSION_COOKIE_NAME}_${companyId}`
res.cookie(compCookieName, req.fdkSession.id, {
secure: true,
httpOnly: true,
expires: sessionExpires,
signed: true,
sameSite: "None"
});
res.header['x-company-id'] = companyId;
req.extension = ext;
if (ext.webhookRegistry.isInitialized && ext.webhookRegistry.isSubscribeOnInstall) {
const client = await ext.getPlatformClient(companyId, req.fdkSession);
Expand All @@ -147,13 +126,31 @@ function setupRoutes(ext) {
}
let redirectUrl = await ext.callbacks.auth(req);
logger.debug(`Redirecting after auth callback to url: ${redirectUrl}`);
res.redirect(redirectUrl);
res.redirect(`${redirectUrl}?token=${req.query.token}`);
} catch (error) {
logger.error(error);
next(error);
}
});

FdkRoutes.get(["/fp/session_token", "/adm/session_token"], sessionMiddleware(true) ,async (req, res, next) => {
let payload = {
current_user: req.fdkSession.current_user.id,
extension_id: req.fdkSession.extension_id
}
req.fdkSession.company_id ?
payload['company_id'] = req.fdkSession.company_id
: payload['organization_id'] = req.fdkSession.organization_id;

const jwtToken = jwt.sign(payload, ext.api_secret);
req.fdkSession.id = jwtToken;
req.fdkSession.expires = new Date(Date.now() + req.fdkSession.expires_in * 1000);
await SessionStorage.saveSession(req.fdkSession);

// delete temp token after use if jwt is issued
ext.storage.del(req.headers.authorization);
return res.status(200).json({token: jwtToken});
})

FdkRoutes.post("/fp/auto_install", sessionMiddleware(false), async (req, res, next) => {
try {
Expand Down Expand Up @@ -230,47 +227,39 @@ function setupRoutes(ext) {
try {
let organizationId = req.query.organization_id;
let partnerConfig = ext.getPartnerConfig(organizationId);

const state = uuidv4();
const payload = {
state: state,
organization_id: organizationId
}
const token = jwt.sign(payload, ext.api_secret);
let session;

session = new Session(Session.generateSessionId(true));
let sessionExpires = new Date(Date.now() + 900000);

session = new Session(token);
if (session.isNew) {
session.organization_id = organizationId;
session.scope = ext.scopes;
session.expires = sessionExpires;
session.access_mode = 'online';
session.extension_id = ext.api_key;
} else {
if (session.expires) {
session.expires = new Date(session.expires);
}
}

session.state = state;
req.fdkSession = session;
req.extension = ext;

const cookieName = ADMIN_SESSION_COOKIE_NAME;
res.cookie(cookieName, session.id, {
secure: true,
httpOnly: true,
expires: session.expires,
signed: true,
sameSite: "none"
});

session.state = uuidv4();

let authCallback = urljoin(ext.base_url, "/adm/auth");

let redirectUrl = partnerConfig.oauthClient.startAuthorization({
scope: session.scope,
redirectUri: authCallback,
redirectUri: `${authCallback}?token=${token}`,
state: session.state,
access_mode: 'online'
})

await SessionStorage.saveSession(session);
await SessionStorage.saveSession(session, true);
logger.debug(`Redirect after partner install callback to url: ${redirectUrl}`);
res.redirect(redirectUrl);

Expand All @@ -280,13 +269,12 @@ function setupRoutes(ext) {
}
})

FdkRoutes.get("/adm/auth", partnerSessionMiddleware(false), async (req, res, next) => {
FdkRoutes.get("/adm/auth", sessionMiddleware(false), async (req, res, next) => {
try {
if (!req.fdkSession) {
throw new FdkSessionNotFoundError("Can not complete oauth process as session not found");
}

if (req.fdkSession.state !== req.query.state) {
if (req.fdkSession.state !== req.query.state || req.fdkSession.organization_id !== req.query.organization_id) {
throw new FdkInvalidOAuthError('Invalid oauth call');
}

Expand All @@ -302,7 +290,7 @@ function setupRoutes(ext) {
token.access_token_validity = sessionExpires.getTime();
req.fdkSession.updateToken(token);

await SessionStorage.saveSession(req.fdkSession);
await SessionStorage.saveSession(req.fdkSession, true);

// offline token
if (!ext.isOnlineAccessMode()) {
Expand Down Expand Up @@ -332,19 +320,9 @@ function setupRoutes(ext) {
await SessionStorage.saveSession(session);
}

const cookieName = ADMIN_SESSION_COOKIE_NAME;

res.cookie(cookieName, req.fdkSession.id, {
secure: true,
httpOnly: true,
expires: sessionExpires,
signed: true,
sameSite: 'none'
})

let redirectUrl = urljoin(ext.base_url, '/admin')
logger.debug(`Redirecting after auth callback to url: ${redirectUrl}`)
res.redirect(redirectUrl);
res.redirect(`${redirectUrl}?token=${req.query.token}`);

} catch(error) {
logger.error(error);
Expand Down
2 changes: 1 addition & 1 deletion express/session/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Session {
refresh_token: this.refresh_token,
expires_in: this.expires_in,
extension_id: this.extension_id,
access_token_validity: this.access_token_validity
access_token_validity: this.access_token_validity,
};
}

Expand Down
Loading