OpenID Connect (OIDC)

Paybin Identity Provider implements OpenID Connect 1.0, an identity layer built on top of OAuth 2.0, enabling secure user authentication and authorization for your applications.

Overview

OpenID Connect (OIDC) provides a standardized way to authenticate users and obtain their basic profile information. Paybin's Identity Provider supports the Authorization Code Flow with PKCE (Proof Key for Code Exchange) for enhanced security.

Key Features

  • Standards-based authentication: Full OpenID Connect 1.0 compliance
  • Secure token management: JWT-based access and ID tokens
  • Flexible scope system: Granular control over user data access
  • PKCE support: Enhanced security for public clients
  • Discovery endpoint: Automatic configuration via well-known endpoint

Supported Grant Types

  • Authorization Code Flow with PKCE (recommended)
  • Authorization Code Flow
  • Refresh Token Flow

Getting Started

Prerequisites

Before integrating OIDC, you'll need:

  1. Client ID: Your application's unique identifier
  2. Client Secret: Secure credential for confidential clients
  3. Redirect URIs: Whitelisted callback URLs for your application
  4. OIDC Configuration: Discovery endpoint URL

Environment URL

https://idp.paybin.io

Discovery Endpoint

Paybin provides an OIDC discovery endpoint for automatic configuration:

https://idp.paybin.io/.well-known/openid-configuration

Authorization Flow

Step 1: Generate PKCE Parameters

For enhanced security, generate PKCE parameters before initiating authorization:

const crypto = require('crypto');

function generatePKCE() {
  // Generate code verifier (43-128 characters)
  const codeVerifier = crypto.randomBytes(32).toString('base64url');

  // Generate code challenge
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return {
    codeVerifier,
    codeChallenge,
    codeChallengeMethod: 'S256'
  };
}

const pkce = generatePKCE();
// Store codeVerifier securely for later use

Step 2: Initiate Authorization Request

Redirect users to the authorization endpoint:

GET https://idp.paybin.io/connect/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=openid profile email merchant:read
  &state=RANDOM_STATE_STRING
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256
  &nonce=RANDOM_NONCE_STRING

Authorization Parameters

ParameterRequiredDescription
response_typeYesMust be code for authorization code flow
client_idYesYour application's client ID
redirect_uriYesWhitelisted callback URL
scopeYesSpace-separated list of requested scopes
stateRecommendedRandom string to prevent CSRF attacks
code_challengeRecommendedPKCE code challenge (base64url-encoded)
code_challenge_methodRecommendedMust be S256 for SHA-256
nonceRecommendedRandom string for replay attack prevention

Step 3: Handle Authorization Callback

After user authentication, Paybin redirects back with an authorization code:

GET https://yourapp.com/callback?
  code=AUTHORIZATION_CODE
  &state=RANDOM_STATE_STRING

Important: Always verify the state parameter matches your original request.

Step 4: Exchange Code for Tokens

Exchange the authorization code for access and ID tokens:

POST https://idp.paybin.io/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&code_verifier=CODE_VERIFIER

Token Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMyIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "def50200a8b7c...",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMyIsInR5cCI6IkpXVCJ9...",
  "scope": "openid profile email merchant:read"
}

Token Management

Access Tokens

Access tokens are JWT tokens used to authenticate API requests:

// Decode access token (JWT)
const jwt = require('jsonwebtoken');

const decoded = jwt.decode(accessToken);
console.log(decoded);

// Example payload:
// {
//   "sub": "user-123",
//   "client_id": "your-client-id",
//   "scope": ["openid", "profile", "email", "merchant:read"],
//   "iat": 1640995200,
//   "exp": 1640998800,
//   "iss": "https://idp.paybin.io",
//   "aud": "paybin-api"
// }

ID Tokens

ID tokens contain user identity information:

// Decode ID token (JWT)
const idToken = jwt.decode(response.id_token);
console.log(idToken);

// Example payload:
// {
//   "sub": "user-123",
//   "email": "user@example.com",
//   "email_verified": true,
//   "name": "John Doe",
//   "given_name": "John",
//   "family_name": "Doe",
//   "picture": "https://cdn.paybin.io/avatars/user-123.jpg",
//   "iat": 1640995200,
//   "exp": 1640998800,
//   "iss": "https://idp.paybin.io",
//   "aud": "your-client-id",
//   "nonce": "RANDOM_NONCE_STRING"
// }

Refresh Tokens

Use refresh tokens to obtain new access tokens without re-authentication:

POST https://idp.paybin.io/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Token Validation

Always validate tokens before trusting their contents:

const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

// Create JWKS client
const client = jwksClient({
  jwksUri: 'https://idp.paybin.io/.well-known/jwks.json'
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

// Verify token
jwt.verify(token, getKey, {
  audience: 'your-client-id',
  issuer: 'https://idp.paybin.io',
  algorithms: ['RS256']
}, (err, decoded) => {
  if (err) {
    console.error('Token validation failed:', err);
    return;
  }
  console.log('Token is valid:', decoded);
});

Available Scopes

Paybin Identity Provider supports the following scopes for granular access control:

Standard OpenID Scopes

ScopeDescriptionClaims Included
openidRequired - Enables OpenID Connect authenticationsub
profileAccess to user's basic profile informationname, given_name, family_name, picture, locale
emailAccess to user's email addressemail, email_verified
phoneAccess to user's phone numberphone_number, phone_number_verified
addressAccess to user's postal addressaddress (structured object)

Paybin-Specific Scopes

ScopeDescriptionRequired Permissions
merchant:readRead merchant account information and balancesMerchant role
merchant:writeCreate and manage merchant transactionsMerchant role + Write permission
merchant:withdrawExecute withdrawal operationsMerchant role + Withdraw permission
transaction:readRead transaction history and detailsUser role
transaction:writeCreate new transactionsUser role + Transaction permission
wallet:readView wallet addresses and balancesUser role
wallet:writeGenerate new wallet addressesUser role + Wallet permission
kyc:readAccess KYC verification statusUser role
kyc:writeSubmit KYC documentsUser role + KYC permission
offline_accessEnables refresh tokens for long-lived accessAny role

Scope Examples

Basic User Authentication

scope=openid profile email

Returns user identity with profile and email information.

Merchant Integration

scope=openid profile email merchant:read merchant:write transaction:read

Enables merchant operations, transaction management, and user identification.

Full Access Application

scope=openid profile email merchant:read merchant:write merchant:withdraw transaction:read transaction:write wallet:read wallet:write offline_access

Complete access to all Paybin features with refresh token support.

Read-Only Access

scope=openid profile email merchant:read transaction:read wallet:read

Read-only access for analytics and reporting applications.

Scope Inheritance

Some scopes automatically include related permissions:

  • merchant:write implies merchant:read
  • merchant:withdraw implies merchant:read and merchant:write
  • transaction:write implies transaction:read
  • wallet:write implies wallet:read

Dynamic Consent

Users will be presented with a consent screen showing exactly what permissions your application is requesting:

YourApp would like to:
✓ Verify your identity (openid)
✓ View your profile information (profile)
✓ View your email address (email)
✓ Read your merchant account balance (merchant:read)
✓ Create transactions (merchant:write)
✓ Access this information offline (offline_access)

User Info Endpoint

Retrieve additional user information using the access token:

GET https://idp.paybin.io/connect/userinfo
Authorization: Bearer ACCESS_TOKEN

Response Example

{
  "sub": "user-123",
  "email": "user@example.com",
  "email_verified": true,
  "name": "John Doe",
  "given_name": "John",
  "family_name": "Doe",
  "picture": "https://cdn.paybin.io/avatars/user-123.jpg",
  "locale": "en-US",
  "merchant_id": "merchant-456",
  "merchant_status": "active",
  "kyc_verified": true
}

Using User Info in Your Application

const axios = require('axios');

async function getUserInfo(accessToken) {
  try {
    const response = await axios.get('https://idp.paybin.io/connect/userinfo', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });

    return response.data;
  } catch (error) {
    console.error('Failed to fetch user info:', error);
    throw error;
  }
}

// Usage
const userInfo = await getUserInfo(accessToken);
console.log(`Welcome ${userInfo.name}!`);

OIDC Discovery

Discovery Document

The discovery endpoint provides all necessary endpoints and configuration:

GET https://idp.paybin.io/.well-known/openid-configuration

Discovery Response

{
  "issuer": "https://idp.paybin.io",
  "authorization_endpoint": "https://idp.paybin.io/connect/authorize",
  "token_endpoint": "https://idp.paybin.io/connect/token",
  "userinfo_endpoint": "https://idp.paybin.io/connect/userinfo",
  "jwks_uri": "https://idp.paybin.io/.well-known/jwks.json",
  "end_session_endpoint": "https://idp.paybin.io/connect/endsession",
  "revocation_endpoint": "https://idp.paybin.io/connect/revocation",
  "introspection_endpoint": "https://idp.paybin.io/connect/introspect",
  "response_types_supported": [
    "code",
    "token",
    "id_token",
    "code id_token",
    "code token",
    "id_token token",
    "code id_token token"
  ],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": [
    "openid",
    "profile",
    "email",
    "phone",
    "address",
    "merchant:read",
    "merchant:write",
    "merchant:withdraw",
    "transaction:read",
    "transaction:write",
    "wallet:read",
    "wallet:write",
    "kyc:read",
    "kyc:write",
    "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "claims_supported": [
    "sub",
    "email",
    "email_verified",
    "name",
    "given_name",
    "family_name",
    "picture",
    "locale",
    "phone_number",
    "phone_number_verified",
    "address",
    "merchant_id",
    "merchant_status",
    "kyc_verified"
  ],
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ]
}

Code Examples

Node.js / Express Integration

const express = require('express');
const session = require('express-session');
const { Issuer, generators } = require('openid-client');

const app = express();

// Configure session
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}));

// Initialize OIDC client
let paybinClient;

async function initializeOIDC() {
  const paybinIssuer = await Issuer.discover('https://idp.paybin.io');

  paybinClient = new paybinIssuer.Client({
    client_id: process.env.PAYBIN_CLIENT_ID,
    client_secret: process.env.PAYBIN_CLIENT_SECRET,
    redirect_uris: ['https://yourapp.com/callback'],
    response_types: ['code']
  });

  console.log('Paybin OIDC client initialized');
}

// Login route
app.get('/login', (req, res) => {
  const codeVerifier = generators.codeVerifier();
  const codeChallenge = generators.codeChallenge(codeVerifier);
  const state = generators.state();
  const nonce = generators.nonce();

  // Store for later verification
  req.session.codeVerifier = codeVerifier;
  req.session.state = state;
  req.session.nonce = nonce;

  const authUrl = paybinClient.authorizationUrl({
    scope: 'openid profile email merchant:read merchant:write',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: state,
    nonce: nonce
  });

  res.redirect(authUrl);
});

// Callback route
app.get('/callback', async (req, res) => {
  try {
    const params = paybinClient.callbackParams(req);

    const tokenSet = await paybinClient.callback(
      'https://yourapp.com/callback',
      params,
      {
        code_verifier: req.session.codeVerifier,
        state: req.session.state,
        nonce: req.session.nonce
      }
    );

    // Store tokens securely
    req.session.accessToken = tokenSet.access_token;
    req.session.refreshToken = tokenSet.refresh_token;
    req.session.idToken = tokenSet.id_token;

    // Get user info
    const userInfo = await paybinClient.userinfo(tokenSet.access_token);
    req.session.user = userInfo;

    res.redirect('/dashboard');
  } catch (error) {
    console.error('Authentication error:', error);
    res.redirect('/login?error=authentication_failed');
  }
});

// Protected route
app.get('/dashboard', (req, res) => {
  if (!req.session.user) {
    return res.redirect('/login');
  }

  res.json({
    message: `Welcome ${req.session.user.name}!`,
    user: req.session.user
  });
});

// Logout route
app.get('/logout', (req, res) => {
  const idToken = req.session.idToken;
  req.session.destroy();

  const logoutUrl = paybinClient.endSessionUrl({
    id_token_hint: idToken,
    post_logout_redirect_uri: 'https://yourapp.com'
  });

  res.redirect(logoutUrl);
});

// Start server
initializeOIDC().then(() => {
  app.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
  });
});

Python / Flask Integration

from flask import Flask, session, redirect, url_for, request
from authlib.integrations.flask_client import OAuth
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# Configure OAuth
oauth = OAuth(app)
paybin = oauth.register(
    name='paybin',
    client_id='YOUR_CLIENT_ID',
    client_secret='YOUR_CLIENT_SECRET',
    server_metadata_url='https://idp.paybin.io/.well-known/openid-configuration',
    client_kwargs={
        'scope': 'openid profile email merchant:read merchant:write',
        'code_challenge_method': 'S256'
    }
)

@app.route('/login')
def login():
    redirect_uri = url_for('callback', _external=True)
    return paybin.authorize_redirect(redirect_uri)

@app.route('/callback')
def callback():
    try:
        token = paybin.authorize_access_token()
        user_info = token.get('userinfo')

        session['user'] = user_info
        session['access_token'] = token['access_token']

        return redirect('/dashboard')
    except Exception as e:
        print(f'Authentication error: {e}')
        return redirect('/login?error=authentication_failed')

@app.route('/dashboard')
def dashboard():
    user = session.get('user')
    if not user:
        return redirect('/login')

    return f"Welcome {user['name']}!"

@app.route('/logout')
def logout():
    session.clear()
    return redirect('https://idp.paybin.io/connect/endsession')

if __name__ == '__main__':
    app.run(debug=True)

C# / ASP.NET Core Integration

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

var builder = WebApplication.CreateBuilder(args);

// Configure authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.Authority = "https://idp.paybin.io";
    options.ClientId = builder.Configuration["Paybin:ClientId"];
    options.ClientSecret = builder.Configuration["Paybin:ClientSecret"];
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.UsePkce = true;

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.Scope.Add("merchant:read");
    options.Scope.Add("merchant:write");
    options.Scope.Add("offline_access");

    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// Protected endpoint
app.MapGet("/dashboard", async (HttpContext context) =>
{
    var accessToken = await context.GetTokenAsync("access_token");
    var user = context.User;

    return Results.Ok(new
    {
        Name = user.FindFirst("name")?.Value,
        Email = user.FindFirst("email")?.Value,
        AccessToken = accessToken
    });
})
.RequireAuthorization();

app.Run();

Best Practices

Security

  1. Always use HTTPS: Never transmit tokens over unencrypted connections
  2. Implement PKCE: Use PKCE for all clients, especially public clients
  3. Validate state parameter: Prevent CSRF attacks by verifying state
  4. Verify nonce: Protect against replay attacks in ID tokens
  5. Validate tokens: Always verify JWT signatures and claims
  6. Secure token storage: Store tokens securely (encrypted, httpOnly cookies)
  7. Rotate secrets: Regularly rotate client secrets

Token Management

  1. Handle token expiration: Implement automatic token refresh
  2. Clear tokens on logout: Remove all tokens when user logs out
  3. Use refresh tokens wisely: Request offline_access only when needed
  4. Monitor token usage: Track and audit token usage patterns
  5. Revoke compromised tokens: Immediately revoke if security breach detected

User Experience

  1. Minimize scope requests: Only request scopes you actually need
  2. Explain permissions: Clearly communicate why you need specific scopes
  3. Handle errors gracefully: Provide user-friendly error messages
  4. Support logout: Implement proper logout with session termination
  5. Remember user choice: Cache consent decisions appropriately

Integration

  1. Use discovery endpoint: Automatically configure endpoints via discovery
  2. Handle rate limits: Implement backoff strategies for token endpoint
  3. Cache JWKS: Cache public keys for token validation
  4. Test thoroughly: Always test your integration thoroughly before production
  5. Monitor logs: Track authentication flows and errors

Error Handling

// Comprehensive error handling
async function handleOIDCError(error) {
  switch (error.error) {
    case 'invalid_request':
      console.error('Invalid OIDC request parameters');
      break;
    case 'invalid_client':
      console.error('Invalid client credentials');
      break;
    case 'invalid_grant':
      console.error('Invalid authorization code or refresh token');
      break;
    case 'unauthorized_client':
      console.error('Client not authorized for this grant type');
      break;
    case 'unsupported_grant_type':
      console.error('Grant type not supported');
      break;
    case 'invalid_scope':
      console.error('Invalid or unsupported scope');
      break;
    case 'access_denied':
      console.error('User denied authorization');
      break;
    default:
      console.error('OIDC error:', error);
  }
}

Support

For OIDC integration support:

Was this page helpful?