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_SECRET=your_client_secret
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
# 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
# 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
- Refresh token flow
- 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
- 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`
- 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 menu upload/get
- Test order lifecycle actions
@ -19,3 +22,9 @@ Validation sequence:
3. Full menu replacement test via PUT
4. Order webhook receipt and lifecycle action tests
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": {
"post": {
"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": {
"post": {
"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)",
"request": {

View File

@ -7,7 +7,7 @@ const envSchema = z.object({
UBER_CLIENT_ID: z.string().min(1),
UBER_CLIENT_SECRET: z.string().min(1),
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"),
SQLITE_PATH: z.string().default("./data/uber_wrapper.db"),
WRAPPER_API_KEY: z.string().optional()
@ -24,4 +24,3 @@ const env = parsed.data;
env.SQLITE_PATH = path.resolve(process.cwd(), env.SQLITE_PATH);
module.exports = env;

View File

@ -1,5 +1,11 @@
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) {
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 = {
getAuthorizeUrl,
oauthCallback,
refreshMerchantToken
refreshMerchantToken,
generateClientCredentialsToken,
getDomainPairingStatus
};

View File

@ -53,9 +53,55 @@ async function refreshToken(refreshToken) {
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 = {
buildAuthorizeUrl,
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));
/**
* @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
* /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));
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;