Skip to main content

OAuth authorization

How Homa Meta Platform starts an authorization request, protects it against CSRF, exchanges the authorization code on the server and revokes access.

Authorization request

Authorization always begins on the server. The browser posts a consent confirmation to an internal route; that route composes the Meta authorization URL and issues a redirect.

The request carries the app identity, the redirect URI, the requested scopes and an unguessable state value. It never carries the app secret.

Authorization request shape
GET {META_AUTHORIZATION_URL}
  ?client_id={META_APP_ID}
  &redirect_uri={META_REDIRECT_URI}
  &state={random-state-value}
  &response_type=code
  &scope={comma-separated-scopes}

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.

EnvironmentRedirect URI
Productionhttps://meta.homacrm.com/authorized
DevelopmentRegistered 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.

  1. Generate a cryptographically random value before redirecting.
  2. Store it in an HttpOnly, Secure, SameSite=Lax cookie with a short lifetime.
  3. Send the same value as the state query parameter.
  4. On return, compare the query value against the cookie using a constant-time comparison.
  5. Clear the cookie immediately after the comparison, whether it succeeded or failed.
  6. Reject the callback if the values differ or the cookie is absent.
Illustrative pseudocode — starting the request
// 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 response

Authorization 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.

Illustrative pseudocode — handling the callback
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.

  1. The user disconnects the integration inside Homa CRM.
  2. The user removes Homa Meta Platform from their Meta account settings, which triggers the deauthorize callback.
  3. 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.