feat: implement sandbox domain mapping checks and client credentials token utility

This commit is contained in:
MOHAN 2026-03-29 17:39:13 +05:30
parent 522f46fd1e
commit fccabf449a
10 changed files with 237 additions and 12 deletions

View File

@ -5,10 +5,10 @@ PORT=8080
UBER_CLIENT_ID=your_client_id UBER_CLIENT_ID=your_client_id
UBER_CLIENT_SECRET=your_client_secret UBER_CLIENT_SECRET=your_client_secret
UBER_REDIRECT_URI=http://localhost:8080/api/v1/auth/uber/callback UBER_REDIRECT_URI=http://localhost:8080/api/v1/auth/uber/callback
UBER_OAUTH_BASE_URL=https://login.uber.com UBER_OAUTH_BASE_URL=https://auth.uber.com
UBER_API_BASE_URL=https://api.uber.com UBER_API_BASE_URL=https://api.uber.com
# Sandbox values when testing: # Sandbox values when testing:
# UBER_OAUTH_BASE_URL=https://sandbox-auth.uber.com # UBER_OAUTH_BASE_URL=https://sandbox-login.uber.com
# UBER_API_BASE_URL=https://test-api.uber.com # UBER_API_BASE_URL=https://test-api.uber.com
# SQLite database path # SQLite database path

View File

@ -6,4 +6,5 @@ Focus: Uber Eats OAuth 2.0 per merchant (multi-client onboarding).
- Handle callback and store tokens - Handle callback and store tokens
- Refresh token flow - Refresh token flow
- Token expiry handling policy - Token expiry handling policy
- Sandbox utility for app token generation (`client_credentials`)
- Domain pairing validation to prevent sandbox/production mismatch

View File

@ -0,0 +1,29 @@
# 10 Sandbox Testing Audit
Source checked: Uber Eats "Sandbox & Testing" section shared by you.
## Implemented Now
- `.env` defaults aligned to production auth/api domains:
- `https://auth.uber.com`
- `https://api.uber.com`
- Sandbox domain guidance aligned:
- `https://sandbox-login.uber.com`
- `https://test-api.uber.com`
- Added `client_credentials` token endpoint for testing utility:
- `POST /api/v1/auth/uber/client-credentials-token`
- Added domain pairing verification endpoint:
- `GET /api/v1/auth/uber/domain-pairing-status`
## Already Implemented Before
- Testing-friendly generic passthrough endpoint
- Webhook ingestion endpoint
- Core auth callback and token refresh paths
## Pending
- Automated test-store provisioning workflow (depends on Uber support process)
- Webhook signature verification (requires exact official webhook signing spec section)
- Full automated sandbox test suite scripts

View File

@ -4,8 +4,11 @@ Checklist:
- Create a dedicated TESTING app in Uber Developer Dashboard - Create a dedicated TESTING app in Uber Developer Dashboard
- Use sandbox domains in `.env`: - Use sandbox domains in `.env`:
- `UBER_OAUTH_BASE_URL=https://sandbox-auth.uber.com` - `UBER_OAUTH_BASE_URL=https://sandbox-login.uber.com`
- `UBER_API_BASE_URL=https://test-api.uber.com` - `UBER_API_BASE_URL=https://test-api.uber.com`
- Ensure domain pairing is valid:
- Testing: `sandbox-login.uber.com -> test-api.uber.com`
- Production: `auth.uber.com -> api.uber.com`
- Test OAuth connect flow - Test OAuth connect flow
- Test menu upload/get - Test menu upload/get
- Test order lifecycle actions - Test order lifecycle actions
@ -19,3 +22,9 @@ Validation sequence:
3. Full menu replacement test via PUT 3. Full menu replacement test via PUT
4. Order webhook receipt and lifecycle action tests 4. Order webhook receipt and lifecycle action tests
5. Error and retry behavior checks 5. Error and retry behavior checks
Troubleshooting quick checks:
- Verify test credentials belong to TESTING app type
- Token/API domain mismatch is the most common failure
- Sandbox data can reset; do not rely on persistence

View File

@ -61,6 +61,19 @@
} }
} }
}, },
"/api/v1/auth/uber/client-credentials-token": {
"post": {
"summary": "Generate Uber client credentials token (sandbox/testing utility)",
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Token generated"
}
}
}
},
"/api/v1/auth/uber/{merchantId}/refresh-token": { "/api/v1/auth/uber/{merchantId}/refresh-token": {
"post": { "post": {
"summary": "Refresh Uber token for merchant", "summary": "Refresh Uber token for merchant",
@ -84,6 +97,19 @@
} }
} }
}, },
"/api/v1/auth/uber/domain-pairing-status": {
"get": {
"summary": "Validate Uber auth and API domain pairing",
"tags": [
"Auth"
],
"responses": {
"200": {
"description": "Domain mapping status"
}
}
}
},
"/api/v1/merchants": { "/api/v1/merchants": {
"post": { "post": {
"summary": "Create or update merchant", "summary": "Create or update merchant",

View File

@ -83,6 +83,64 @@
} }
} }
}, },
{
"name": "Check Domain Pairing",
"request": {
"method": "GET",
"header": [
{
"key": "x-api-key",
"value": "{{apiKey}}"
}
],
"url": {
"raw": "{{baseUrl}}/api/v1/auth/uber/domain-pairing-status",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"v1",
"auth",
"uber",
"domain-pairing-status"
]
}
}
},
{
"name": "Client Credentials Token",
"request": {
"method": "POST",
"header": [
{
"key": "x-api-key",
"value": "{{apiKey}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"scope\": \"eats.order\"\n}"
},
"url": {
"raw": "{{baseUrl}}/api/v1/auth/uber/client-credentials-token",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"v1",
"auth",
"uber",
"client-credentials-token"
]
}
}
},
{ {
"name": "Replace Menu (PUT)", "name": "Replace Menu (PUT)",
"request": { "request": {

View File

@ -7,7 +7,7 @@ const envSchema = z.object({
UBER_CLIENT_ID: z.string().min(1), UBER_CLIENT_ID: z.string().min(1),
UBER_CLIENT_SECRET: z.string().min(1), UBER_CLIENT_SECRET: z.string().min(1),
UBER_REDIRECT_URI: z.string().url(), UBER_REDIRECT_URI: z.string().url(),
UBER_OAUTH_BASE_URL: z.string().url().default("https://login.uber.com"), UBER_OAUTH_BASE_URL: z.string().url().default("https://auth.uber.com"),
UBER_API_BASE_URL: z.string().url().default("https://api.uber.com"), UBER_API_BASE_URL: z.string().url().default("https://api.uber.com"),
SQLITE_PATH: z.string().default("./data/uber_wrapper.db"), SQLITE_PATH: z.string().default("./data/uber_wrapper.db"),
WRAPPER_API_KEY: z.string().optional() WRAPPER_API_KEY: z.string().optional()
@ -24,4 +24,3 @@ const env = parsed.data;
env.SQLITE_PATH = path.resolve(process.cwd(), env.SQLITE_PATH); env.SQLITE_PATH = path.resolve(process.cwd(), env.SQLITE_PATH);
module.exports = env; module.exports = env;

View File

@ -1,5 +1,11 @@
const { merchantRepository, uberConnectionRepository } = require("../../db/adapter"); const { merchantRepository, uberConnectionRepository } = require("../../db/adapter");
const { buildAuthorizeUrl, exchangeCodeForToken, refreshToken } = require("./auth.service"); const {
buildAuthorizeUrl,
exchangeCodeForToken,
refreshToken,
getClientCredentialsToken,
getDomainMappingStatus
} = require("./auth.service");
function parseExpiresAt(expiresInSeconds) { function parseExpiresAt(expiresInSeconds) {
if (!expiresInSeconds) { if (!expiresInSeconds) {
@ -84,9 +90,32 @@ async function refreshMerchantToken(req, res) {
}); });
} }
async function generateClientCredentialsToken(req, res) {
const scope = req.body?.scope || "eats.order";
const tokenData = await getClientCredentialsToken({ scope });
return res.json({
success: true,
message: "Client credentials token generated",
data: tokenData
});
}
async function getDomainPairingStatus(req, res) {
const status = getDomainMappingStatus();
return res.json({
success: true,
data: status,
message:
status.isValid || status.isLegacy
? "Domain mapping looks usable"
: "Domain mapping is invalid. Use sandbox-login.uber.com->test-api.uber.com or auth.uber.com->api.uber.com."
});
}
module.exports = { module.exports = {
getAuthorizeUrl, getAuthorizeUrl,
oauthCallback, oauthCallback,
refreshMerchantToken refreshMerchantToken,
generateClientCredentialsToken,
getDomainPairingStatus
}; };

View File

@ -53,9 +53,55 @@ async function refreshToken(refreshToken) {
return data; return data;
} }
async function getClientCredentialsToken({ scope = "eats.order" } = {}) {
const payload = new URLSearchParams({
client_id: env.UBER_CLIENT_ID,
client_secret: env.UBER_CLIENT_SECRET,
grant_type: "client_credentials",
scope
});
const { data } = await uberAuthClient.post("/oauth/v2/token", payload.toString(), {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
return data;
}
function hostFromUrl(urlValue) {
return new URL(urlValue).host.toLowerCase();
}
function getDomainMappingStatus() {
const authHost = hostFromUrl(env.UBER_OAUTH_BASE_URL);
const apiHost = hostFromUrl(env.UBER_API_BASE_URL);
const pair = `${authHost}->${apiHost}`;
const validPairs = [
"sandbox-login.uber.com->test-api.uber.com",
"auth.uber.com->api.uber.com"
];
const legacyPairs = ["login.uber.com->api.uber.com"];
const isValid = validPairs.includes(pair);
const isLegacy = legacyPairs.includes(pair);
return {
authBaseUrl: env.UBER_OAUTH_BASE_URL,
apiBaseUrl: env.UBER_API_BASE_URL,
pair,
isValid,
isLegacy,
expectedPairs: validPairs
};
}
module.exports = { module.exports = {
buildAuthorizeUrl, buildAuthorizeUrl,
exchangeCodeForToken, exchangeCodeForToken,
refreshToken refreshToken,
getClientCredentialsToken,
getDomainMappingStatus
}; };

View File

@ -36,6 +36,22 @@ router.get("/uber/authorize-url", asyncHandler(controller.getAuthorizeUrl));
*/ */
router.get("/uber/callback", asyncHandler(controller.oauthCallback)); router.get("/uber/callback", asyncHandler(controller.oauthCallback));
/**
* @openapi
* /api/v1/auth/uber/client-credentials-token:
* post:
* summary: Generate Uber client credentials token (sandbox/testing utility)
* tags:
* - Auth
* responses:
* 200:
* description: Token generated
*/
router.post(
"/uber/client-credentials-token",
asyncHandler(controller.generateClientCredentialsToken)
);
/** /**
* @openapi * @openapi
* /api/v1/auth/uber/{merchantId}/refresh-token: * /api/v1/auth/uber/{merchantId}/refresh-token:
@ -55,5 +71,17 @@ router.get("/uber/callback", asyncHandler(controller.oauthCallback));
*/ */
router.post("/uber/:merchantId/refresh-token", asyncHandler(controller.refreshMerchantToken)); router.post("/uber/:merchantId/refresh-token", asyncHandler(controller.refreshMerchantToken));
module.exports = router; /**
* @openapi
* /api/v1/auth/uber/domain-pairing-status:
* get:
* summary: Validate Uber auth and API domain pairing
* tags:
* - Auth
* responses:
* 200:
* description: Domain mapping status
*/
router.get("/uber/domain-pairing-status", asyncHandler(controller.getDomainPairingStatus));
module.exports = router;