OAuth Flow Quickstart
This guide provides a complete walkthrough with working code examples for implementing the OAuth flow. For a conceptual overview of how OAuth works, see Getting Credentials via OAuth.
Prerequisites
Before you begin, ensure you have:
- Your own integrator service account credentials and an authenticated noon session for your backend. If you have not set this up yet, start with Getting Your Credentials and Authenticating Your Requests.
- OAuth client credentials (client_id and client_secret) provided by noon
- At least one redirect URI registered with noon for receiving authorization codes. A client can have several — see Choosing a Callback URL
- Basic understanding of OAuth 2.0 authorization code flow
When your backend calls POST /identity/oauth/v1/token/create and POST /identity/oauth/v1/token/exchange, those requests must be sent with an authenticated noon session created from your integrator service account credentials.
The OAuth client_id and client_secret identify your application during the code exchange, but they do not make the request authenticated on their own. The seller-scoped credential is returned only after the exchange succeeds.
Implementation Guide
Step 1: Redirect User to Authorization URL
When a seller wants to connect their noon account to your platform, generate a PKCE code_verifier, derive its code_challenge, store the verifier server-side, then redirect them to the noon authorization endpoint:
https://oauth.noon.partners/?client_id=abc123xyz&state=random-state-string-123&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
Query Parameters:
| Parameter | Description | Example |
|---|---|---|
client_id | Your OAuth client ID | abc123xyz |
state | Random string for CSRF protection | random-state-string-123 |
code_challenge | Base64url, unpadded, of SHA-256(code_verifier) | E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM |
code_challenge_method | Must be S256 | S256 |
redirect_uri | Optional. Which registered callback URL to return to. Omitted, the first one registered is used | https://yourapp.com/callback |
The seller will be presented with a consent screen showing what permissions your application is requesting. They must approve before proceeding.
code_challenge and code_challenge_method are required whenever your OAuth client is configured to require PKCE — the seller will see an error on the consent screen if they are missing. For other clients they are optional per flow, but recommended. Full details, including how to generate the verifier, are in Securing the Flow with PKCE.
Whichever applies, store the code_verifier alongside state in your server-side session record for this flow. You need it in Step 3, and it must never be sent to the browser.
Step 2: Handle Authorization Callback
After the seller approves, noon redirects them back to your registered redirect URI with the authorization code:
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=random-state-string-123&iss=https%3A%2F%2Foauth.noon.partners
Your callback handler must:
- Verify the state parameter matches what you sent to prevent CSRF attacks
- Extract the authorization code from the
codeparameter - Load the
code_verifieryou stored for this flow, if you started the flow with acode_challenge - Ignore, or optionally check,
iss— it names the authorization server that issued the code (https://oauth.noon.partners). Do not treat it as an unexpected parameter and fail
Example callback handling:
from flask import Flask, request, redirect
app = Flask(__name__)
@app.route('/callback')
def oauth_callback():
# Verify state parameter
state = request.args.get('state')
if state != session.get('oauth_state'): # session refers to any server-side storage you use to track state for that user request
return "Invalid state parameter", 400
# Optional: confirm which authorization server issued this code (RFC 9207)
issuer = request.args.get('iss')
if issuer and issuer != 'https://oauth.noon.partners':
return "Unexpected issuer", 400
# Get authorization code
auth_code = request.args.get('code')
if not auth_code:
return "No authorization code received", 400
# Proceed to exchange code for token
return exchange_code_for_token(auth_code)
Step 3: Exchange Authorization Code for Access Token
Make a server-to-server API call to exchange the authorization code for an access token.
Use the same authenticated integrator session for this request.
Endpoint: POST /identity/oauth/v1/token/create
Request Body:
{
"grant_type": "authorization_code",
"code": "AUTH_CODE_HERE",
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}
Include code_verifier when this flow's authorization URL carried a code_challenge, and send the verifier matching that exact challenge. Omit it entirely for flows that did not use PKCE — sending one for a non-PKCE flow is rejected. See Securing the Flow with PKCE.
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "TOKEN_TYPE_BEARER",
"expires_in": "3600s",
"scopes": ["access:grant"],
"project_code": "PRJ12345"
}
Never expose your client_secret in client-side code. This exchange must happen on your backend server.
Important Response Fields:
access_token: JWT token needed for the next step (valid for 1 hour and single-use)project_code: The seller's project code to which the service account will have accessexpires_in: Token validity duration
Step 4: Create Service Account and Receive Credentials
Finally, use the access token to create the service account and receive its credentials.
This request must also reuse your authenticated integrator session.
Endpoint: POST /identity/oauth/v1/token/exchange
Request Body:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response:
{
"status": {
"code": 0
},
"project_code": "PRJ12345",
"oauth_request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"result": {
"key_id": "key-abc123",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQ...",
"channel_identifier": "[email protected]",
"project_code": "PRJ12345",
"type": "apijwt",
"issued_at": "2026-04-20T12:00:00Z"
}
}
Important Response Fields:
status: Indicates whether the workflow executed successfully (code: 0means success)project_code: The seller's project codeoauth_request_id: A unique identifier for tracking this OAuth exchange request — store this to monitor the service account creation progressresult: The service account credentials, including the private key needed to authenticate API calls
The result contains the private key, which is only returned once — noon does not store it. Store it in a secrets manager immediately. If you lose it, you can create a new credential via the API User Service.
Save the oauth_request_id from the response. You can use it to check the status of the request in the OAuth tab of the noon Partners Access App. This is helpful for debugging or confirming that the workflow completed successfully.
Complete Code Examples
Here are complete, working examples in multiple programming languages:
These examples assume your backend has already authenticated with noon using your integrator service account credentials and is reusing that authenticated client or session when calling POST /identity/oauth/v1/token/create and POST /identity/oauth/v1/token/exchange.
The OAuth client_id and client_secret shown in the request body identify your OAuth application, but they do not replace the authenticated integrator session. For the standard login flow, see Authenticating Your Requests.
Each example generates a code_verifier per flow, sends the derived code_challenge on the authorization URL, and presents the verifier at token creation. PKCE is required for OAuth clients configured to require it and recommended for all others — see Securing the Flow with PKCE.
If your client does not use PKCE, drop the code_challenge parameters from the authorization URL and the code_verifier field from the token creation body. Sending one without the other is rejected.
Each example sends redirect_uri on the authorization URL to name which registered callback URL the
seller returns to, and shows where to check the iss parameter that comes back with the code. Both
are optional — drop redirect_uri to use the first URL registered on your client. See
Choosing a Callback URL.
- Python
- Go
- Node.js
- PHP
- Java
- .NET
- Curl/Bash
import base64
import hashlib
import json
import os
import uuid
import requests
from urllib.parse import quote
from flask import Flask, request, session, redirect
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # Use a secure secret key
# Load your OAuth client credentials
CLIENT_ID = 'your_client_id'
CLIENT_SECRET = 'your_client_secret'
# One of the callback URLs registered on your OAuth client. Omit it from the authorization URL
# and the seller is returned to the first one registered.
REDIRECT_URI = 'https://your-app.com/oauth/callback'
# The authorization server that issues your codes, echoed back as `iss` on the callback.
ISSUER = 'https://oauth.noon.partners'
# PKCE: 32 random bytes base64url-encode to a 43-character verifier,
# which satisfies the required 43-128 character length
def generate_pkce_pair():
verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('ascii').rstrip('=')
digest = hashlib.sha256(verifier.encode('ascii')).digest()
challenge = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
return verifier, challenge
# Step 1: Initiate OAuth flow - redirect user to authorization URL
@app.route('/connect-seller')
def connect_seller():
# Generate and store state for CSRF protection
state = str(uuid.uuid4())
session['oauth_state'] = state
# Only the challenge is sent - the verifier stays server-side
code_verifier, code_challenge = generate_pkce_pair()
session['code_verifier'] = code_verifier
authorization_url = (
f"https://oauth.noon.partners/?client_id={CLIENT_ID}&state={state}"
f"&redirect_uri={quote(REDIRECT_URI, safe='')}"
f"&code_challenge={code_challenge}&code_challenge_method=S256"
)
return redirect(authorization_url)
# Step 2: Handle callback and verify state
@app.route('/oauth/callback')
def oauth_callback():
# Verify state parameter to prevent CSRF attacks
received_state = request.args.get('state')
stored_state = session.get('oauth_state')
if not received_state or received_state != stored_state:
return "Invalid state parameter - possible CSRF attack", 400
# Clear the stored state
session.pop('oauth_state', None)
# Optional: confirm which authorization server issued this code (RFC 9207)
if request.args.get('iss') not in (None, ISSUER):
return "Unexpected issuer", 400
# Get authorization code
authorization_code = request.args.get('code')
if not authorization_code:
return "No authorization code received", 400
# Retrieve the PKCE verifier stored for this flow
code_verifier = session.pop('code_verifier', None)
if not code_verifier:
return "No code_verifier stored for this flow", 400
try:
# Step 3: Exchange authorization code for access token
token_response = get_access_token(authorization_code, code_verifier)
print(f"Access token obtained for project: {token_response['project_code']}")
# Step 4: Exchange access token to create service account and receive credentials
sa_response = create_service_account(token_response['access_token'])
credentials = sa_response['result']
print(f"Credentials received for project: {token_response['project_code']}")
# Store credentials['private_key'] securely — it is only returned once
return f"Successfully connected seller project: {token_response['project_code']}"
except Exception as e:
return f"Error: {e}", 500
# Step 3: Exchange authorization code for access token
def get_access_token(auth_code, code_verifier):
url = 'https://noon-api-gateway.noon.partners/identity/oauth/v1/token/create'
payload = {
'grant_type': 'authorization_code',
'code': auth_code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code_verifier': code_verifier
}
response = requests.post(url, json=payload, headers={
'Content-Type': 'application/json',
'User-Agent': 'YourApp/1.0'
})
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to get access token: {response.text}")
# Step 4: Exchange access token to create service account and receive credentials
def create_service_account(access_token):
url = 'https://noon-api-gateway.noon.partners/identity/oauth/v1/token/exchange'
payload = {
'access_token': access_token
}
response = requests.post(url, json=payload, headers={
'Content-Type': 'application/json',
'User-Agent': 'YourApp/1.0'
})
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to create service account: {response.text}")
if __name__ == '__main__':
app.run(debug=True)
package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
const (
ClientID = "your_client_id"
ClientSecret = "your_client_secret"
BaseURL = "https://noon-api-gateway.noon.partners/identity/oauth/v1"
// One of the callback URLs registered on your OAuth client.
RedirectURI = "https://your-app.com/oauth/callback"
)
type TokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
CodeVerifier string `json:"code_verifier,omitempty"`
}
// PKCE: 32 random bytes base64url-encode to a 43-character verifier,
// which satisfies the required 43-128 character length.
// RawURLEncoding is base64url without padding, exactly what the spec requires.
func generatePKCEPair() (verifier string, challenge string, err error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", "", err
}
verifier = base64.RawURLEncoding.EncodeToString(buf)
digest := sha256.Sum256([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(digest[:])
return verifier, challenge, nil
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn string `json:"expires_in"`
Scopes []string `json:"scopes"`
ProjectCode string `json:"project_code"`
}
type ExchangeRequest struct {
AccessToken string `json:"access_token"`
}
type ExchangeResponse struct {
Status struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"status"`
ProjectCode string `json:"project_code"`
}
func getAccessToken(authCode string, codeVerifier string) (*TokenResponse, error) {
reqBody := TokenRequest{
GrantType: "authorization_code",
Code: authCode,
ClientID: ClientID,
ClientSecret: ClientSecret,
CodeVerifier: codeVerifier,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
resp, err := http.Post(
BaseURL+"/token/create",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get token: %s", string(body))
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, err
}
return &tokenResp, nil
}
func createServiceAccount(accessToken string) (*ExchangeResponse, error) {
reqBody := ExchangeRequest{
AccessToken: accessToken,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
resp, err := http.Post(
BaseURL+"/token/exchange",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to create service account: %s", string(body))
}
var exchangeResp ExchangeResponse
if err := json.NewDecoder(resp.Body).Decode(&exchangeResp); err != nil {
return nil, err
}
return &exchangeResp, nil
}
func main() {
// Step 1: Direct user to authorization URL
// Store codeVerifier server-side against this flow - you need it in Step 3
codeVerifier, codeChallenge, err := generatePKCEPair()
if err != nil {
fmt.Printf("Error generating PKCE pair: %v\n", err)
return
}
// RedirectURI must be one of the callback URLs registered on your OAuth client. Omit the
// parameter and the seller is returned to the first one registered.
authURL := fmt.Sprintf(
"https://oauth.noon.partners/?client_id=%s&state=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256",
ClientID,
uuid.New().String(),
url.QueryEscape(RedirectURI),
codeChallenge,
)
fmt.Printf("Direct user to: %s\n", authURL)
// Step 2: Receive authorization code from callback. The callback also carries `iss` naming the
// authorization server that issued the code - compare it if you validate it, ignore it otherwise.
authCode := "AUTHORIZATION_CODE_FROM_CALLBACK"
// Step 3: Get access token, presenting the verifier for this flow
tokenResp, err := getAccessToken(authCode, codeVerifier)
if err != nil {
fmt.Printf("Error getting access token: %v\n", err)
return
}
fmt.Printf("Access token obtained for project: %s\n", tokenResp.ProjectCode)
// Step 4: Create service account
saResp, err := createServiceAccount(tokenResp.AccessToken)
if err != nil {
fmt.Printf("Error creating service account: %v\n", err)
return
}
fmt.Printf("Service account created for project: %s\n", saResp.ProjectCode)
}
import express from "express";
import session from "express-session";
import axios from "axios";
import { createHash, randomBytes } from "crypto";
const app = express();
const CLIENT_ID = "your_client_id";
const CLIENT_SECRET = "your_client_secret";
// One of the callback URLs registered on your OAuth client. Omit it from the authorization URL
// and the seller is returned to the first one registered.
const REDIRECT_URI = "https://your-app.com/oauth/callback";
// The authorization server that issues your codes, echoed back as `iss` on the callback.
const ISSUER = "https://oauth.noon.partners";
const BASE_URL = "https://noon-api-gateway.noon.partners/identity/oauth/v1";
// Setup session middleware
app.use(session({
secret: 'your-secret-key-here',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));
// PKCE: 32 random bytes base64url-encode to a 43-character verifier,
// which satisfies the required 43-128 character length
function generatePkcePair() {
const codeVerifier = randomBytes(32).toString('base64url');
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
return { codeVerifier, codeChallenge };
}
// Step 1: Initiate OAuth flow
app.get('/connect-seller', (req, res) => {
// Generate and store state for CSRF protection
const state = randomBytes(16).toString('hex');
req.session.oauthState = state;
// Only the challenge is sent - the verifier stays server-side
const { codeVerifier, codeChallenge } = generatePkcePair();
req.session.codeVerifier = codeVerifier;
const authUrl = `https://oauth.noon.partners/?client_id=${CLIENT_ID}&state=${state}`
+ `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`
+ `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
res.redirect(authUrl);
});
// Step 2: Handle OAuth callback
app.get('/oauth/callback', async (req, res) => {
// Verify state parameter
const receivedState = req.query.state;
const storedState = req.session.oauthState;
if (!receivedState || receivedState !== storedState) {
return res.status(400).send('Invalid state parameter - possible CSRF attack');
}
// Clear stored state
delete req.session.oauthState;
// Optional: confirm which authorization server issued this code (RFC 9207)
if (req.query.iss && req.query.iss !== ISSUER) {
return res.status(400).send('Unexpected issuer');
}
const authCode = req.query.code;
if (!authCode) {
return res.status(400).send('No authorization code received');
}
// Retrieve the PKCE verifier stored for this flow
const codeVerifier = req.session.codeVerifier;
delete req.session.codeVerifier;
if (!codeVerifier) {
return res.status(400).send('No code_verifier stored for this flow');
}
try {
// Step 3: Exchange code for access token
const tokenResponse = await getAccessToken(authCode, codeVerifier);
console.log(`Access token obtained for project: ${tokenResponse.project_code}`);
// Step 4: Create service account
const saResponse = await createServiceAccount(tokenResponse.access_token);
console.log('Service account created:', saResponse);
res.send(`Successfully connected seller project: ${tokenResponse.project_code}`);
} catch (error) {
console.error('Error:', error.message);
res.status(500).send(`Error: ${error.message}`);
}
});
// Step 3: Exchange authorization code for access token
async function getAccessToken(authCode, codeVerifier) {
try {
const response = await axios.post(
`${BASE_URL}/token/create`,
{
grant_type: "authorization_code",
code: authCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: codeVerifier,
},
{
headers: {
"Content-Type": "application/json",
"User-Agent": "YourApp/1.0",
},
}
);
return response.data;
} catch (error) {
throw new Error(`Failed to get access token: ${error.response?.data || error.message}`);
}
}
// Step 4: Exchange access token to create service account
async function createServiceAccount(accessToken) {
try {
const response = await axios.post(
`${BASE_URL}/token/exchange`,
{
access_token: accessToken,
},
{
headers: {
"Content-Type": "application/json",
"User-Agent": "YourApp/1.0",
},
}
);
return response.data;
} catch (error) {
throw new Error(`Failed to create service account: ${error.response?.data || error.message}`);
}
}
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('Visit http://localhost:3000/connect-seller to start OAuth flow');
});
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$clientId = "your_client_id";
$clientSecret = "your_client_secret";
$baseUrl = "https://noon-api-gateway.noon.partners/identity/oauth/v1";
// One of the callback URLs registered on your OAuth client. Omit it from the authorization URL
// and the seller is returned to the first one registered.
$redirectUri = "https://your-app.com/oauth/callback";
// The authorization server that issues your codes, echoed back as `iss` on the callback.
$issuer = "https://oauth.noon.partners";
// PKCE: 32 random bytes base64url-encode to a 43-character verifier,
// which satisfies the required 43-128 character length
function base64UrlEncode(string $bytes): string {
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
// Step 1: Initiate OAuth flow
// Generate state and store it in session for CSRF protection
session_start();
$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;
// Only the challenge is sent - the verifier stays server-side
$codeVerifier = base64UrlEncode(random_bytes(32));
$codeChallenge = base64UrlEncode(hash('sha256', $codeVerifier, true));
$_SESSION['code_verifier'] = $codeVerifier;
$authUrl = sprintf(
"https://oauth.noon.partners/?client_id=%s&state=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256",
$clientId,
$state,
urlencode($redirectUri),
$codeChallenge
);
// Redirect user to this URL
header("Location: " . $authUrl);
exit();
// Step 2: Handle OAuth callback (in your callback endpoint)
// Verify state parameter
session_start();
$receivedState = $_GET['state'] ?? '';
$storedState = $_SESSION['oauth_state'] ?? '';
// Optional: confirm which authorization server issued this code (RFC 9207)
$receivedIssuer = $_GET['iss'] ?? '';
if ($receivedIssuer !== '' && $receivedIssuer !== $issuer) {
http_response_code(400);
exit('Unexpected issuer');
}
if (empty($receivedState) || $receivedState !== $storedState) {
die("Invalid state parameter - possible CSRF attack");
}
// Clear stored state
unset($_SESSION['oauth_state']);
// Get authorization code
$authorizationCode = $_GET['code'] ?? '';
if (empty($authorizationCode)) {
die("No authorization code received");
}
// Retrieve the PKCE verifier stored for this flow
$storedVerifier = $_SESSION['code_verifier'] ?? '';
unset($_SESSION['code_verifier']);
if (empty($storedVerifier)) {
die("No code_verifier stored for this flow");
}
$client = new Client();
try {
// Step 3: Exchange authorization code for access token
$tokenResponse = $client->post($baseUrl . '/token/create', [
'json' => [
'grant_type' => 'authorization_code',
'code' => $authorizationCode,
'client_id' => $clientId,
'client_secret' => $clientSecret,
'code_verifier' => $storedVerifier,
],
'headers' => [
'Content-Type' => 'application/json',
'User-Agent' => 'YourApp/1.0',
]
]);
$tokenData = json_decode($tokenResponse->getBody(), true);
echo "Access token obtained for project: " . $tokenData['project_code'] . "\n";
// Step 4: Exchange access token to create service account
$saResponse = $client->post($baseUrl . '/token/exchange', [
'json' => [
'access_token' => $tokenData['access_token'],
],
'headers' => [
'Content-Type' => 'application/json',
'User-Agent' => 'YourApp/1.0',
]
]);
$saData = json_decode($saResponse->getBody(), true);
echo "Service account created: " . json_encode($saData) . "\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
package com.noon.oauth;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public class OAuthExample {
private static final String CLIENT_ID = "your_client_id";
private static final String CLIENT_SECRET = "your_client_secret";
private static final String BASE_URL = "https://noon-api-gateway.noon.partners/identity/oauth/v1";
// One of the callback URLs registered on your OAuth client. Omit it from the authorization URL
// and the seller is returned to the first one registered.
private static final String REDIRECT_URI = "https://your-app.com/oauth/callback";
// The authorization server that issues your codes, echoed back as `iss` on the callback.
private static final String ISSUER = "https://oauth.noon.partners";
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
public static class TokenRequest {
public String grant_type = "authorization_code";
public String code;
public String client_id;
public String client_secret;
public String code_verifier;
}
// PKCE: 32 random bytes base64url-encode to a 43-character verifier,
// which satisfies the required 43-128 character length
public static String generateCodeVerifier() {
byte[] buf = new byte[32];
new SecureRandom().nextBytes(buf);
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
}
public static String deriveCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
public static class TokenResponse {
public String access_token;
public String token_type;
public String expires_in;
public String[] scopes;
public String project_code;
}
public static class ExchangeRequest {
public String access_token;
}
public static class ExchangeResponse {
public static class Status {
public int code;
public String message;
}
public Status status;
public String project_code;
}
public TokenResponse getAccessToken(String authCode, String codeVerifier) throws IOException {
TokenRequest req = new TokenRequest();
req.code = authCode;
req.client_id = CLIENT_ID;
req.client_secret = CLIENT_SECRET;
req.code_verifier = codeVerifier;
String json = mapper.writeValueAsString(req);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(BASE_URL + "/token/create")
.post(body)
.addHeader("User-Agent", "YourApp/1.0")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to get token: " + response);
}
return mapper.readValue(response.body().string(), TokenResponse.class);
}
}
public ExchangeResponse createServiceAccount(String accessToken) throws IOException {
ExchangeRequest req = new ExchangeRequest();
req.access_token = accessToken;
String json = mapper.writeValueAsString(req);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(BASE_URL + "/token/exchange")
.post(body)
.addHeader("User-Agent", "YourApp/1.0")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to create service account: " + response);
}
return mapper.readValue(response.body().string(), ExchangeResponse.class);
}
}
public static void main(String[] args) {
// Step 1: Initiate OAuth flow
// Generate state and store it (e.g., in session/database) for CSRF protection
String state = UUID.randomUUID().toString();
// IMPORTANT: Store this state value to verify later in callback
// Example: session.setAttribute("oauth_state", state);
OAuthExample oauth = new OAuthExample();
try {
// Only the challenge is sent - store the verifier server-side alongside state
String codeVerifier = generateCodeVerifier();
String codeChallenge = deriveCodeChallenge(codeVerifier);
// Example: session.setAttribute("code_verifier", codeVerifier);
String authUrl = String.format(
"https://oauth.noon.partners/?client_id=%s&state=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256",
CLIENT_ID,
state,
URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8),
codeChallenge
);
// Redirect user to authUrl
System.out.println("Redirect user to: " + authUrl);
// Step 2: Handle OAuth callback (in your callback endpoint)
// IMPORTANT: Verify state parameter matches stored value
// String receivedState = request.getParameter("state");
// String storedState = (String) session.getAttribute("oauth_state");
// if (!receivedState.equals(storedState)) {
// throw new SecurityException("Invalid state - possible CSRF attack");
// }
// IMPORTANT: Load the verifier stored for this flow
// String codeVerifier = (String) session.getAttribute("code_verifier");
// Optional: confirm which authorization server issued this code (RFC 9207)
// String receivedIssuer = request.getParameter("iss");
// if (receivedIssuer != null && !ISSUER.equals(receivedIssuer)) {
// throw new SecurityException("Unexpected issuer");
// }
String authCode = "AUTHORIZATION_CODE_FROM_CALLBACK";
// Step 3: Get access token, presenting the verifier for this flow
TokenResponse tokenResp = oauth.getAccessToken(authCode, codeVerifier);
System.out.println("Access token obtained for project: " + tokenResp.project_code);
// Step 4: Create service account
ExchangeResponse saResp = oauth.createServiceAccount(tokenResp.access_token);
System.out.println("Service account created for project: " + saResp.project_code);
} catch (IOException | NoSuchAlgorithmException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
class OAuthExample
{
private const string ClientId = "your_client_id";
private const string ClientSecret = "your_client_secret";
private const string BaseUrl = "https://noon-api-gateway.noon.partners/identity/oauth/v1";
// One of the callback URLs registered on your OAuth client. Omit it from the authorization URL
// and the seller is returned to the first one registered.
private const string RedirectUri = "https://your-app.com/oauth/callback";
// The authorization server that issues your codes, echoed back as `iss` on the callback.
private const string Issuer = "https://oauth.noon.partners";
private static readonly HttpClient client = new HttpClient();
public class TokenRequest
{
public string grant_type { get; set; } = "authorization_code";
public string code { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
public string code_verifier { get; set; }
}
// PKCE: 32 random bytes base64url-encode to a 43-character verifier,
// which satisfies the required 43-128 character length
static string Base64UrlEncode(byte[] bytes) =>
Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
static string GenerateCodeVerifier() =>
Base64UrlEncode(RandomNumberGenerator.GetBytes(32));
static string DeriveCodeChallenge(string codeVerifier) =>
Base64UrlEncode(SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier)));
public class TokenResponse
{
public string access_token { get; set; }
public string token_type { get; set; }
public string expires_in { get; set; }
public string[] scopes { get; set; }
public string project_code { get; set; }
}
public class ExchangeRequest
{
public string access_token { get; set; }
}
public class ExchangeResponse
{
public class StatusInfo
{
public int code { get; set; }
public string message { get; set; }
}
public StatusInfo status { get; set; }
public string project_code { get; set; }
}
static async Task<TokenResponse> GetAccessToken(string authCode, string codeVerifier)
{
var request = new TokenRequest
{
code = authCode,
client_id = ClientId,
client_secret = ClientSecret,
code_verifier = codeVerifier
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync($"{BaseUrl}/token/create", content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TokenResponse>(responseBody);
}
static async Task<ExchangeResponse> CreateServiceAccount(string accessToken)
{
var request = new ExchangeRequest
{
access_token = accessToken
};
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync($"{BaseUrl}/token/exchange", content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ExchangeResponse>(responseBody);
}
static async Task Main(string[] args)
{
// Step 1: Initiate OAuth flow
// Generate state and store it (e.g., in session) for CSRF protection
var state = Guid.NewGuid().ToString();
// IMPORTANT: Store this state value to verify later in callback
// Example: HttpContext.Session.SetString("oauth_state", state);
// Only the challenge is sent - store the verifier server-side alongside state
var codeVerifier = GenerateCodeVerifier();
var codeChallenge = DeriveCodeChallenge(codeVerifier);
// Example: HttpContext.Session.SetString("code_verifier", codeVerifier);
var authUrl = $"https://oauth.noon.partners/?client_id={ClientId}&state={state}"
+ $"&redirect_uri={Uri.EscapeDataString(RedirectUri)}"
+ $"&code_challenge={codeChallenge}&code_challenge_method=S256";
// Redirect user to authUrl
Console.WriteLine($"Redirect user to: {authUrl}");
// Step 2: Handle OAuth callback (in your callback endpoint)
// IMPORTANT: Verify state parameter matches stored value
// var receivedState = Request.Query["state"];
// var storedState = HttpContext.Session.GetString("oauth_state");
// if (receivedState != storedState) {
// throw new SecurityException("Invalid state - possible CSRF attack");
// }
// IMPORTANT: Load the verifier stored for this flow
// var codeVerifier = HttpContext.Session.GetString("code_verifier");
// Optional: confirm which authorization server issued this code (RFC 9207)
// var receivedIssuer = Request.Query["iss"].ToString();
// if (!string.IsNullOrEmpty(receivedIssuer) && receivedIssuer != Issuer) {
// throw new SecurityException("Unexpected issuer");
// }
var authCode = "AUTHORIZATION_CODE_FROM_CALLBACK";
try
{
// Step 3: Get access token, presenting the verifier for this flow
var tokenResponse = await GetAccessToken(authCode, codeVerifier);
Console.WriteLine($"Access token obtained for project: {tokenResponse.project_code}");
// Step 4: Create service account
var saResponse = await CreateServiceAccount(tokenResponse.access_token);
Console.WriteLine($"Service account created for project: {saResponse.project_code}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
#!/usr/bin/env bash
CLIENT_ID="your_client_id"
CLIENT_SECRET="your_client_secret"
BASE_URL="https://noon-api-gateway.noon.partners/identity/oauth/v1"
# One of the callback URLs registered on your OAuth client. Omit redirect_uri from the
# authorization URL and the seller is returned to the first one registered.
REDIRECT_URI="https://your-app.com/oauth/callback"
# The authorization server that issues your codes, echoed back as `iss` on the callback.
ISSUER="https://oauth.noon.partners"
# Step 1: Initiate OAuth flow
# Generate state and store it for CSRF protection
STATE=$(uuidgen)
# IMPORTANT: Store this state value (e.g., in Redis, file, or database)
# to verify later in callback. Example:
# echo "$STATE" > /tmp/oauth_state_${USER_ID}
# PKCE: 32 random bytes base64url-encoded (no padding) gives a 43-character verifier
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
CODE_CHALLENGE=$(printf '%s' "${CODE_VERIFIER}" \
| openssl dgst -sha256 -binary \
| openssl base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
# IMPORTANT: Store the verifier server-side next to the state. Example:
# echo "$CODE_VERIFIER" > /tmp/oauth_verifier_${USER_ID}
REDIRECT_URI_ENC=$(printf '%s' "${REDIRECT_URI}" | jq -sRr @uri)
AUTH_URL="https://oauth.noon.partners/?client_id=${CLIENT_ID}&state=${STATE}&redirect_uri=${REDIRECT_URI_ENC}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256"
echo "Redirect user to: ${AUTH_URL}"
# Step 2: Handle OAuth callback (in your callback handler)
# IMPORTANT: Verify state parameter matches stored value
# RECEIVED_STATE="..." # Extract from callback URL
# STORED_STATE=$(cat /tmp/oauth_state_${USER_ID})
# Optional: confirm which authorization server issued this code (RFC 9207)
# RECEIVED_ISS="..." # Extract the iss parameter from the callback URL
# [ -z "$RECEIVED_ISS" ] || [ "$RECEIVED_ISS" = "$ISSUER" ] || { echo "Unexpected issuer"; exit 1; }
# if [ "$RECEIVED_STATE" != "$STORED_STATE" ]; then
# echo "Invalid state - possible CSRF attack"
# exit 1
# fi
AUTH_CODE="AUTHORIZATION_CODE_FROM_CALLBACK"
# IMPORTANT: Load the verifier stored for this flow. Example:
# CODE_VERIFIER=$(cat /tmp/oauth_verifier_${USER_ID})
# Step 3: Exchange authorization code for access token
TOKEN_RESPONSE=$(curl -s -X POST "${BASE_URL}/token/create" \
-H "Content-Type: application/json" \
-H "User-Agent: YourApp/1.0" \
-d "{
\"grant_type\": \"authorization_code\",
\"code\": \"${AUTH_CODE}\",
\"client_id\": \"${CLIENT_ID}\",
\"client_secret\": \"${CLIENT_SECRET}\",
\"code_verifier\": \"${CODE_VERIFIER}\"
}")
# Extract access token
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
PROJECT_CODE=$(echo "$TOKEN_RESPONSE" | jq -r '.project_code')
echo "Access token obtained for project: ${PROJECT_CODE}"
# Step 4: Exchange access token to create service account
SA_RESPONSE=$(curl -s -X POST "${BASE_URL}/token/exchange" \
-H "Content-Type: application/json" \
-H "User-Agent: YourApp/1.0" \
-d "{
\"access_token\": \"${ACCESS_TOKEN}\"
}")
echo "Service account created: ${SA_RESPONSE}"
Testing Your Integration
Verification Checklist
Before going live with sellers, verify:
- State parameter is validated to prevent CSRF
- A fresh
code_verifieris generated per flow and never reused - The
code_verifieris stored server-side and never reaches the browser - The
code_verifiersent at token creation belongs to the same flow as the authorization code - Authorization codes are used only once
- Access tokens are stored securely and never logged
- Token expiry is handled gracefully
- Client secret is never exposed to clients
- Error responses are handled appropriately
Security Best Practices
For comprehensive security guidelines, see the Security Considerations in the Getting Credentials via OAuth guide.
1. Protect Your Client Secret
# BAD - Hardcoded secret
client_secret = "my-secret-123"
# GOOD - Use environment variables
import os
client_secret = os.environ.get('NOON_CLIENT_SECRET')
2. Validate State Parameter
# Always validate state
import secrets
# Generate random state
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# Later, in callback
if request.args.get('state') != session.get('oauth_state'):
raise ValueError("CSRF detected")
3. Generate a Fresh PKCE Verifier Per Flow
import base64
import hashlib
import os
def generate_pkce_pair():
# 32 random bytes -> 43-character base64url verifier, no padding
verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('ascii').rstrip('=')
digest = hashlib.sha256(verifier.encode('ascii')).digest()
challenge = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
return verifier, challenge
# Keep the verifier server-side, next to the state you already store
verifier, challenge = generate_pkce_pair()
session['code_verifier'] = verifier
# Only the challenge goes on the authorization URL
4. Use HTTPS Only
# Enforce HTTPS in production
if not request.is_secure and app.env == 'production':
return redirect(request.url.replace('http://', 'https://'))
5. Use Access Tokens Immediately
# Exchange access token immediately after receiving it
# Access tokens are single-use and should be consumed right away
token_response = get_access_token(auth_code)
access_token = token_response['access_token']
# Immediately exchange for service account creation
sa_response = create_service_account(access_token)
Next Steps
Now that you've completed the OAuth flow:
- Store the credentials from
resultsecurely — Save the private key in a secrets manager and associate it with the seller in your database - Start making API calls — Use the credentials as shown in the Authentication Guide
- Rotate or revoke credentials as needed — Use the API User Service to manage credentials over time
Additional Resources
- OAuth API Reference - Complete API documentation
- Getting Credentials via OAuth - Detailed OAuth concepts
Need Help?
If you encounter issues:
- Review the error messages in the API response
- Check the Error Handling section for common issues
- Contact Support with your client_id (never share client_secret)