Authorization Flow
Authorization Flow
The authorization flow redirects a user to SeloraX where they log in and grant your application permission to access their profile data. On approval, SeloraX redirects back to your website with a short-lived authorization code.
Step 1: Redirect to Authorize
Redirect the user's browser to the SeloraX authorization endpoint:
GET /api/oauth/authorize?response_type=code&client_id=sx_oc_...&redirect_uri=https://example.com/callback&scope=openid+profile+email&state=random_string
Query Parameters
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Your client ID (format: sx_oc_...) |
redirect_uri | Yes | Must exactly match one of your registered redirect_uris |
scope | Yes | Space-separated list of scopes (e.g. openid profile email) |
state | Recommended | Random string to prevent CSRF. Verify this on callback. |
code_challenge | Conditional | Required for public clients. PKCE challenge. |
code_challenge_method | Conditional | S256 (recommended) or plain |
nonce | Optional | Random string bound to the session for replay protection |
store_id | Optional | For merchants with multiple stores, specify which store context to use |
:::tip
Always include state to protect against CSRF attacks. Generate a cryptographically random string, store it in the user's session, and verify it matches when the callback is received.
:::
Example: Building the Authorization URL
const params = new URLSearchParams({
response_type: 'code',
client_id: 'sx_oc_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6',
redirect_uri: 'https://example.com/auth/callback',
scope: 'openid profile email',
state: crypto.randomUUID(),
});
const authorizeUrl = `https://api.selorax.io/api/oauth/authorize?${params}`;
// Redirect user to authorizeUrlStep 2: User Consent
The user must be logged into SeloraX. The platform determines their user type automatically:
- Customers (
req.user.isAdmin === false) → identified ascustomer - Merchants (
req.user.isAdmin === true) → identified asmerchant
If consent was previously granted
If the user already approved your app with the same (or broader) scopes, SeloraX skips the consent screen and returns the authorization code immediately:
{
"redirect_url": "https://example.com/callback?code=sx_ic_...&state=random_string",
"status": 200
}If consent is needed
SeloraX returns consent screen data for the dashboard to render:
{
"consent_required": true,
"client": {
"name": "My Website",
"logo_url": "https://example.com/logo.png",
"homepage_url": "https://example.com",
"description": "E-commerce analytics dashboard"
},
"requested_scopes": [
{ "code": "openid", "name": "OpenID", "description": "Verify your identity" },
{ "code": "profile", "name": "Profile", "description": "Access your name and avatar" },
{ "code": "email", "name": "Email", "description": "Access your email address" }
],
"user": {
"name": "John Doe",
"user_type": "customer"
},
"status": 200
}Step 3: Approve or Decline Consent
After the user makes their choice, submit the decision:
POST /api/oauth/authorize/consent
Request Body
{
"client_id": "sx_oc_...",
"redirect_uri": "https://example.com/auth/callback",
"scope": "openid profile email",
"state": "random_string",
"approved": true,
"code_challenge": null,
"code_challenge_method": null,
"nonce": null,
"store_id": null
}| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | Yes | Your client ID |
redirect_uri | string | Yes | Must match the original authorize request |
scope | string | Yes | Space-separated scopes |
state | string | No | Passed through to the redirect URL |
approved | boolean | Yes | true to grant, false to deny |
code_challenge | string | No | PKCE challenge (if using PKCE) |
code_challenge_method | string | No | S256 or plain |
nonce | string | No | OIDC nonce |
store_id | integer | No | Store context for merchants |
If approved
Consent is saved (so the user won't be asked again for the same scopes), and an authorization code is generated:
{
"redirect_url": "https://example.com/callback?code=sx_ic_...&state=random_string",
"status": 200
}If declined
{
"redirect_url": "https://example.com/callback?error=access_denied&state=random_string",
"status": 200
}Step 4: Receive the Callback
Your callback endpoint receives the authorization code as a query parameter:
GET https://example.com/auth/callback?code=sx_ic_...&state=random_string
- Verify
statematches what you stored in the user's session - If
erroris present, handle the denial (e.g.error=access_denied) - Exchange the code for tokens
:::warning Authorization codes expire after 60 seconds. Exchange them immediately. :::
Consent Persistence
Once a user approves your application for a set of scopes, their consent is stored. On subsequent visits:
- If the requested scopes are the same or a subset of previously granted scopes → consent is skipped
- If you request new scopes not previously granted → the consent screen is shown again
Consent is stored per unique combination of client_id, user_id, and user_type.
Rate Limiting
Both the authorization and consent endpoints are rate-limited to 30 requests per 15 minutes per IP address. If exceeded, the server responds with:
{
"message": "Too many authorization requests.",
"status": 429
}This protects against brute-force consent attacks and authorization code enumeration.
Error Handling
| Error | Description |
|---|---|
invalid_client | Client ID not found or client is inactive |
invalid_redirect_uri | Redirect URI doesn't match any registered URI |
invalid_scope | One or more requested scopes are invalid or not allowed for this client |
invalid_request | Missing required parameters, or PKCE required but not provided |
access_denied | User declined the consent screen |
429 | Too many requests — rate limit exceeded |