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:
- Client ID: Your application's unique identifier
- Client Secret: Secure credential for confidential clients
- Redirect URIs: Whitelisted callback URLs for your application
- 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
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code for authorization code flow |
client_id | Yes | Your application's client ID |
redirect_uri | Yes | Whitelisted callback URL |
scope | Yes | Space-separated list of requested scopes |
state | Recommended | Random string to prevent CSRF attacks |
code_challenge | Recommended | PKCE code challenge (base64url-encoded) |
code_challenge_method | Recommended | Must be S256 for SHA-256 |
nonce | Recommended | Random 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
| Scope | Description | Claims Included |
|---|---|---|
openid | Required - Enables OpenID Connect authentication | sub |
profile | Access to user's basic profile information | name, given_name, family_name, picture, locale |
email | Access to user's email address | email, email_verified |
phone | Access to user's phone number | phone_number, phone_number_verified |
address | Access to user's postal address | address (structured object) |
Paybin-Specific Scopes
| Scope | Description | Required Permissions |
|---|---|---|
merchant:read | Read merchant account information and balances | Merchant role |
merchant:write | Create and manage merchant transactions | Merchant role + Write permission |
merchant:withdraw | Execute withdrawal operations | Merchant role + Withdraw permission |
transaction:read | Read transaction history and details | User role |
transaction:write | Create new transactions | User role + Transaction permission |
wallet:read | View wallet addresses and balances | User role |
wallet:write | Generate new wallet addresses | User role + Wallet permission |
kyc:read | Access KYC verification status | User role |
kyc:write | Submit KYC documents | User role + KYC permission |
offline_access | Enables refresh tokens for long-lived access | Any 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:writeimpliesmerchant:readmerchant:withdrawimpliesmerchant:readandmerchant:writetransaction:writeimpliestransaction:readwallet:writeimplieswallet: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
- Always use HTTPS: Never transmit tokens over unencrypted connections
- Implement PKCE: Use PKCE for all clients, especially public clients
- Validate state parameter: Prevent CSRF attacks by verifying state
- Verify nonce: Protect against replay attacks in ID tokens
- Validate tokens: Always verify JWT signatures and claims
- Secure token storage: Store tokens securely (encrypted, httpOnly cookies)
- Rotate secrets: Regularly rotate client secrets
Token Management
- Handle token expiration: Implement automatic token refresh
- Clear tokens on logout: Remove all tokens when user logs out
- Use refresh tokens wisely: Request
offline_accessonly when needed - Monitor token usage: Track and audit token usage patterns
- Revoke compromised tokens: Immediately revoke if security breach detected
User Experience
- Minimize scope requests: Only request scopes you actually need
- Explain permissions: Clearly communicate why you need specific scopes
- Handle errors gracefully: Provide user-friendly error messages
- Support logout: Implement proper logout with session termination
- Remember user choice: Cache consent decisions appropriately
Integration
- Use discovery endpoint: Automatically configure endpoints via discovery
- Handle rate limits: Implement backoff strategies for token endpoint
- Cache JWKS: Cache public keys for token validation
- Test thoroughly: Always test your integration thoroughly before production
- 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:
- Review our Authentication Guide
- Check our Quick Start Guide
- Contact support: support@paybin.io
Always test your OIDC integration thoroughly before deploying to production. Use the discovery endpoint at https://idp.paybin.io/.well-known/openid-configuration.