feat: implement sandbox domain mapping checks and client credentials token utility
This commit is contained in:
parent
522f46fd1e
commit
fccabf449a
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
29
docs/developer-portal/10-sandbox-testing-audit.md
Normal file
29
docs/developer-portal/10-sandbox-testing-audit.md
Normal 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
|
||||||
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user