CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-medusa-interfaces

Core interfaces for Medusa e-commerce framework service implementations

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

oauth-service.mddocs/

OAuth Service

Interface for OAuth service implementations providing token generation, refresh, and destruction operations for OAuth-based authentication flows.

Capabilities

Static Methods

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;

Core OAuth Operations

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;

Implementation Example

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()}`;
  }
}

Usage in Medusa

OAuth services are typically used for:

  • Admin Authentication: JWT tokens for admin panel access
  • Customer Authentication: Customer login sessions
  • API Access: Third-party integrations with OAuth providers
  • Service-to-Service: Inter-service authentication

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 };
  }
}

Token Validation Middleware

// 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" });
    }
  };
}

Error Handling

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"

Security Considerations

When implementing OAuth services:

  • Use strong secrets for token signing
  • Implement token rotation for refresh tokens
  • Set appropriate expiry times for different token types
  • Maintain token blacklists for revoked tokens
  • Use HTTPS for all token exchanges
  • Validate token audience and issuer claims
  • Implement rate limiting for token endpoints

docs

base-service.md

file-service.md

fulfillment-service.md

index.md

notification-service.md

oauth-service.md

payment-service.md

search-service.md

tile.json