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_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
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user