Core interfaces for Medusa e-commerce framework service implementations
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Interface for OAuth service implementations providing token generation, refresh, and destruction operations for OAuth-based authentication flows.
Type checking and identification methods for OAuth services.
/**
* Static property identifying this as an OAuth service
*/
static _isOauthService: boolean;
/**
* Checks if an object is an OAuth service
* @param {object} obj - Object to check
* @returns {boolean} True if obj is an OAuth service
*/
static isOauthService(obj: object): boolean;Abstract methods that must be implemented by child classes for OAuth token management.
/**
* Generates an OAuth token for authentication
* @returns {any} Generated token result
* @throws {Error} If not overridden by child class
*/
generateToken(): any;
/**
* Refreshes an existing OAuth token
* @returns {any} Refreshed token result
* @throws {Error} If not overridden by child class
*/
refreshToken(): any;
/**
* Destroys/revokes an OAuth token
* @returns {any} Token destruction result
* @throws {Error} If not overridden by child class
*/
destroyToken(): any;import { OauthService } from "medusa-interfaces";
import jwt from "jsonwebtoken";
import crypto from "crypto";
class JWTOauthService extends OauthService {
constructor(options) {
super();
this.secretKey = options.secret_key;
this.refreshTokenSecret = options.refresh_token_secret;
this.accessTokenExpiry = options.access_token_expiry || "15m";
this.refreshTokenExpiry = options.refresh_token_expiry || "7d";
this.tokenStorage = options.token_storage || {}; // In-memory storage for demo
}
async generateToken(payload) {
// Generate access token
const accessToken = jwt.sign(
{
...payload,
type: "access",
jti: crypto.randomUUID() // Unique token ID
},
this.secretKey,
{ expiresIn: this.accessTokenExpiry }
);
// Generate refresh token
const refreshTokenId = crypto.randomUUID();
const refreshToken = jwt.sign(
{
jti: refreshTokenId,
type: "refresh",
user_id: payload.user_id
},
this.refreshTokenSecret,
{ expiresIn: this.refreshTokenExpiry }
);
// Store refresh token (in production, use Redis/database)
this.tokenStorage[refreshTokenId] = {
user_id: payload.user_id,
created_at: new Date(),
is_active: true
};
return {
access_token: accessToken,
refresh_token: refreshToken,
token_type: "Bearer",
expires_in: this.parseExpiry(this.accessTokenExpiry),
scope: payload.scope || "read write"
};
}
async refreshToken(refreshTokenString) {
try {
// Verify refresh token
const decoded = jwt.verify(refreshTokenString, this.refreshTokenSecret);
if (decoded.type !== "refresh") {
throw new Error("Invalid token type");
}
// Check if refresh token is still active
const storedToken = this.tokenStorage[decoded.jti];
if (!storedToken || !storedToken.is_active) {
throw new Error("Refresh token revoked or invalid");
}
// Generate new access token
const newAccessToken = jwt.sign(
{
user_id: storedToken.user_id,
type: "access",
jti: crypto.randomUUID()
},
this.secretKey,
{ expiresIn: this.accessTokenExpiry }
);
return {
access_token: newAccessToken,
token_type: "Bearer",
expires_in: this.parseExpiry(this.accessTokenExpiry)
};
} catch (error) {
throw new Error(`Token refresh failed: ${error.message}`);
}
}
async destroyToken(tokenString, tokenType = "access") {
try {
let decoded;
if (tokenType === "access") {
decoded = jwt.verify(tokenString, this.secretKey);
} else if (tokenType === "refresh") {
decoded = jwt.verify(tokenString, this.refreshTokenSecret);
// Mark refresh token as inactive
if (this.tokenStorage[decoded.jti]) {
this.tokenStorage[decoded.jti].is_active = false;
}
}
// In production, you might maintain a blacklist of revoked tokens
// until their natural expiration
return {
success: true,
token_id: decoded.jti,
revoked_at: new Date()
};
} catch (error) {
throw new Error(`Token destruction failed: ${error.message}`);
}
}
parseExpiry(expiry) {
// Convert JWT expiry format to seconds
const unit = expiry.slice(-1);
const value = parseInt(expiry.slice(0, -1));
switch (unit) {
case "s": return value;
case "m": return value * 60;
case "h": return value * 3600;
case "d": return value * 86400;
default: return 900; // 15 minutes default
}
}
}
// OAuth2 Provider Implementation
class OAuth2Service extends OauthService {
constructor(options) {
super();
this.clientId = options.client_id;
this.clientSecret = options.client_secret;
this.providerUrl = options.provider_url;
this.redirectUri = options.redirect_uri;
}
async generateToken(authorizationCode) {
// Exchange authorization code for tokens
const tokenRequest = {
grant_type: "authorization_code",
code: authorizationCode,
client_id: this.clientId,
client_secret: this.clientSecret,
redirect_uri: this.redirectUri
};
try {
const response = await fetch(`${this.providerUrl}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(tokenRequest)
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
const tokenData = await response.json();
return {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
token_type: tokenData.token_type || "Bearer",
expires_in: tokenData.expires_in,
scope: tokenData.scope
};
} catch (error) {
throw new Error(`OAuth token generation failed: ${error.message}`);
}
}
async refreshToken(refreshTokenString) {
const refreshRequest = {
grant_type: "refresh_token",
refresh_token: refreshTokenString,
client_id: this.clientId,
client_secret: this.clientSecret
};
try {
const response = await fetch(`${this.providerUrl}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(refreshRequest)
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.statusText}`);
}
const tokenData = await response.json();
return {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token || refreshTokenString,
token_type: tokenData.token_type || "Bearer",
expires_in: tokenData.expires_in,
scope: tokenData.scope
};
} catch (error) {
throw new Error(`OAuth token refresh failed: ${error.message}`);
}
}
async destroyToken(tokenString, tokenType = "access") {
const revokeRequest = {
token: tokenString,
token_type_hint: tokenType,
client_id: this.clientId,
client_secret: this.clientSecret
};
try {
const response = await fetch(`${this.providerUrl}/oauth/revoke`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(revokeRequest)
});
// OAuth2 revoke endpoint typically returns 200 even for invalid tokens
return {
success: response.ok,
revoked_at: new Date()
};
} catch (error) {
throw new Error(`OAuth token revocation failed: ${error.message}`);
}
}
// Helper method to generate authorization URL
getAuthorizationUrl(scopes = [], state = null) {
const params = new URLSearchParams({
response_type: "code",
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: scopes.join(" ")
});
if (state) {
params.set("state", state);
}
return `${this.providerUrl}/oauth/authorize?${params.toString()}`;
}
}OAuth services are typically used for:
Basic Usage Pattern:
// In a Medusa authentication service
class AuthService {
constructor({ oauthService }) {
this.oauthService_ = oauthService;
}
async login(credentials) {
// Validate credentials
const user = await this.validateCredentials(credentials);
// Generate OAuth tokens
const tokens = await this.oauthService_.generateToken({
user_id: user.id,
email: user.email,
scope: user.role
});
return {
user: user,
...tokens
};
}
async refreshUserToken(refreshToken) {
try {
const newTokens = await this.oauthService_.refreshToken(refreshToken);
return newTokens;
} catch (error) {
throw new Error("Token refresh failed - please login again");
}
}
async logout(accessToken) {
await this.oauthService_.destroyToken(accessToken, "access");
return { success: true };
}
}// Express middleware for token validation
function createAuthMiddleware(oauthService) {
return async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing or invalid authorization header" });
}
const token = authHeader.substring(7);
try {
// In a real implementation, you'd verify the token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: "Invalid or expired token" });
}
};
}All abstract methods throw descriptive errors when not implemented:
"generateToken must be overridden by the child class""refreshToken must be overridden by the child class""destroyToken must be overridden by the child class"When implementing OAuth services: