OAuth authorization
How Homa Meta Platform starts an authorization request, protects it against CSRF, exchanges the authorization code on the server and revokes access.
Redirect URI
The redirect URI must be registered in the Meta App Dashboard and must match the value sent in the authorization request exactly. Homa uses a single production redirect target.
| Environment | Redirect URI |
|---|---|
| Production | https://meta.homacrm.com/authorized |
| Development | Registered separately in the development app |
- Always use HTTPS in production.
- Do not append query parameters to the registered redirect URI.
- Treat a trailing slash as significant; register the exact form you send.
- Register development hosts only in the development app, never in the production app.
State parameter and CSRF protection
The state parameter binds the redirect back to the browser session that started it. Without it, an attacker could deliver their own authorization code to a signed-in user and attach an account the user never intended to connect.
- Generate a cryptographically random value before redirecting.
- Store it in an HttpOnly, Secure, SameSite=Lax cookie with a short lifetime.
- Send the same value as the state query parameter.
- On return, compare the query value against the cookie using a constant-time comparison.
- Clear the cookie immediately after the comparison, whether it succeeded or failed.
- Reject the callback if the values differ or the cookie is absent.
// Server-only route handler.
const state = crypto.randomUUID()
const url = new URL(process.env.META_AUTHORIZATION_URL!)
url.searchParams.set('client_id', process.env.META_APP_ID!)
url.searchParams.set('redirect_uri', process.env.META_REDIRECT_URI!)
url.searchParams.set('response_type', 'code')
url.searchParams.set('state', state)
const response = NextResponse.redirect(url)
response.cookies.set('meta_oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 600,
})
return responseAuthorization code handling and token exchange
The authorization code is single-use and short-lived. Exchange it immediately, server-side, then discard it. The code must never be logged, stored, or echoed back to the browser.
const returnedState = searchParams.get('state')
const code = searchParams.get('code')
const expectedState = cookies().get('meta_oauth_state')?.value
// Always clear the single-use state cookie first.
cookies().delete('meta_oauth_state')
if (!code || !returnedState || !expectedState) {
return redirect('/auth-error')
}
if (!timingSafeEqual(returnedState, expectedState)) {
return redirect('/auth-error')
}
// Server-to-server. The secret never leaves this process.
const token = await exchangeCodeForToken({
code,
appId: process.env.META_APP_ID!,
appSecret: process.env.META_APP_SECRET!,
redirectUri: process.env.META_REDIRECT_URI!,
})
await storeEncryptedToken({ tenantId, token })
return redirect('/authorized')Token storage
Tokens are written to encrypted storage keyed by tenant and never returned to a client. See the tokens page for the full lifecycle.
- Encrypt at rest using a key held outside the application database.
- Scope every read by tenant identifier so one customer cannot reach another customer’s token.
- Never place a token in a URL, a query string, a log line or an analytics event.
- Never send a token to the browser, even over HTTPS.
Error handling
Every failure path ends at /auth-error with a generic message. The page explains the likely causes without disclosing provider responses.
- User cancelled
- Meta returns an error parameter instead of a code. Offer a retry; this is not a fault condition.
- Invalid or missing state
- Reject the callback and restart the flow. Do not attempt an exchange.
- Redirect URI mismatch
- A configuration error. Compare the registered URI in the App Dashboard against META_REDIRECT_URI.
- Missing permission
- The user declined a scope. Explain which capability is unavailable rather than repeating the raw scope name.
Revocation
Access can end from either side. Both paths must converge on the same cleanup.
- The user disconnects the integration inside Homa CRM.
- The user removes Homa Meta Platform from their Meta account settings, which triggers the deauthorize callback.
- A token is revoked or expires and cannot be refreshed.
- Delete the stored token and mark the connection inactive.
- Stop all polling and outbound calls for that asset.
- Record the revocation in the audit log with an actor and a timestamp.
- Follow the data deletion process for associated data where applicable.
Never exposing secrets in browser code
- META_APP_SECRET is read only inside server-only modules and route handlers.
- No secret is ever prefixed with NEXT_PUBLIC_.
- The client component on /connect posts a consent confirmation and nothing else; it holds no app identity.
- Client code never receives an access token, an authorization code or a state value.
- Rotate the app secret if it is ever printed to a log, committed to version control or pasted into a support ticket.