0% found this document useful (0 votes)
5 views26 pages

Testing Finding

The AP-SSO Platform Developer Integration Guide provides detailed instructions for integrating applications with the Single Sign-On (SSO) system used by government applications in Andhra Pradesh. It outlines prerequisites, integration models (JWT-as-Identity, API Token Exchange, OAuth2/OIDC), and offers step-by-step implementation guidance for various programming languages. The document also includes security best practices and troubleshooting tips to ensure a smooth integration process.

Uploaded by

panditsaurabh98
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views26 pages

Testing Finding

The AP-SSO Platform Developer Integration Guide provides detailed instructions for integrating applications with the Single Sign-On (SSO) system used by government applications in Andhra Pradesh. It outlines prerequisites, integration models (JWT-as-Identity, API Token Exchange, OAuth2/OIDC), and offers step-by-step implementation guidance for various programming languages. The document also includes security best practices and troubleshooting tips to ensure a smooth integration process.

Uploaded by

panditsaurabh98
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

AP-SSO Platform — Developer Integration Guide

Version: 1.0 · Last Updated: March 2026

This guide provides step-by-step instructions for integrating your application with the AP-SSO Platform. Share this document with your development team.

Table of Contents
1. Overview
2. Prerequisites
3. Choosing an Integration Model
4. Model 1 — JWT-as-Identity
5. Model 2 — API Token Exchange
6. Model 3 — OAuth2 / OIDC (Recommended)
7. Using the JavaScript SDK
8. User Data Reference
9. OIDC Discovery & JWKS
10. Security Best Practices
11. Troubleshooting
12. API Reference

Overview
The AP-SSO Platform provides Single Sign-On for all government applications in Andhra Pradesh. Once a user authenticates on any SSO-integrated application, they
can access all other integrated applications without logging in again.

The platform supports three integration models:

Model Name Direction Best For

Apps launched from the SSO Launchpad


1 JWT-as-Identity Launchpad → Your App
portal

Launchpad → Your App → Auth


2 API Token Exchange Apps needing server-side identity verification
Server

3 OAuth2/OIDC Your App ↔ Auth Server Standalone apps with "Login with SSO" button

Prerequisites

1. Register Your Application

Before integrating, contact the SSO Platform team to register your application. You will receive:

Credential Description Required For

Auth Service Base URL of the SSO auth service (e.g.,


All models
URL [Link] )

Application ID UUID assigned to your app in the platform registry Model 1, 2

Client ID OAuth2 client identifier (e.g., sso_a1b2c3d4e5f6... ) Model 2, 3

Client Secret OAuth2 client secret — keep this server-side only! Model 2

Model 2
API Key Alternative to Client Secret for Model 2
(optional)

Redirect URI(s) Callback URL(s) registered for your app Model 3

JWKS URL {authServiceUrl}/.well-known/[Link] Model 1

2. Install the SDK (Optional but Recommended)


We provide official SDKs that handle integration complexity for you. All SDKs are hosted on our private Nexus repository — you must configure your package manager
to use it before installing.

Nexus Repository URL: [Link]

JavaScript / TypeScript (npm)

Step 1: Configure npm to use the Nexus registry for @ap-sso packages. Create or edit .npmrc in your project root:

# .npmrc
@ap-sso:registry=[Link]

Step 2: Install the SDK:

npm install @ap-sso/auth-sdk

Python (pip)

Step 1: Install from the Nexus PyPI repository:

pip install ap-sso-auth \


--index-url [Link] \
--trusted-host [Link]

Or configure permanently in [Link] / [Link] :

# ~/.pip/[Link] (Linux/Mac) or %APPDATA%\pip\[Link] (Windows)


[global]
extra-index-url = [Link]
trusted-host = [Link]

Kotlin / Android (Gradle)

Step 1: Add the Nexus Maven repository to your [Link] :

// [Link]
repositories {
mavenCentral()
maven {
url = uri("[Link]
isAllowInsecureProtocol = true // Remove when using HTTPS
}
}

Step 2: Add the dependency:

dependencies {
implementation("[Link]:auth-kotlin:1.0.0")
}

C# / .NET (NuGet)

Step 1: Add the Nexus NuGet source:

dotnet nuget add source [Link] \


--name ap-sso-nexus

Or add a [Link] to your project root:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="[Link]" value="[Link] />
<add key="ap-sso-nexus" value="[Link] />
</packageSources>
</configuration>

Step 2: Install the SDK:

dotnet add package [Link]

PHP (Composer)

The PHP SDK is distributed as a zip archive from Nexus. Download and extract:

# Download the SDK


curl -O [Link]

# Extract into your project's vendor directory


unzip [Link] -d vendor/apcfss/sso-auth

Then require it in your code:

require_once 'vendor/apcfss/sso-auth/src/[Link]';

Swift (SPM)

The Swift SDK is available as a Swift Package. Add it to your [Link] :

dependencies: [
.package(url: "[Link] from: "1.0.0")
]

Note: The Swift SDK is distributed via Git, not Nexus. Contact the SSO team for repository access.

SDK Availability Summary

Nexus
Language Package Name Status
Repository

JavaScript/TypeScript @ap-sso/auth-sdk npm-hosted


Published

Python ap-sso-auth pypi-hosted


Published

Kotlin/Android [Link]:auth-kotlin maven-hosted


Published

C# / .NET [Link] nuget-hosted Available

PHP ap-sso-auth-sdk raw-hosted


Published

Swift APSSOAuth Git (SPM) Available

3. Environment Setup

Set up these environment variables in your application:


# Required for all models
SSO_AUTH_SERVICE_URL=[Link]

# Model 1
SSO_JWKS_URL=[Link]

# Model 2 & 3
SSO_CLIENT_ID=your_client_id
SSO_CLIENT_SECRET=your_client_secret # NEVER commit this to version control!

# Model 3
SSO_REDIRECT_URI=[Link]

Choosing an Integration Model

┌──────────────────────────────────┐
│ How do users access your app? │
└──────────────┬───────────────────┘

┌──────────────┴──────────────┐
│ │
From SSO Launchpad Directly (standalone)
│ │
┌───────┴───────┐ │
│ │ │
Frontend-only Has Backend Use Model 3
(static app) (server) (OAuth2/OIDC)
│ │
Use Model 1 Use Model 2
(JWT-as-ID) (API Exchange)

Criteria Model 1 Model 2 Model 3

User comes from


Required Not needed
Launchpad Required

Your app has a backend Not needed Either


Required

Standalone "Login with SSO"

(with PKCE for


Secret stays server-side N/A
SPAs)

SSO across multiple apps

Complexity Low Medium Medium

Model 1 — JWT-as-Identity

When to Use

Your application is launched from the SSO Launchpad. The user is already authenticated — you just need to know who they are.

How It Works
┌──────────┐ ┌──────────────┐ ┌─────────────┐
│ User │ │ SSO │ │ Your App │
│ │ │ Launchpad │ │ │
└────┬─────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
│ 1. Click app icon │ │
│─────────────────────>│ │
│ │ │
│ │ 2. Generate signed │
│ │ JWT (60s TTL) │
│ │ │
│ 3. Redirect with JWT in URL │
│<──────────────────────────────────────────────>│
│ [Link] │
│ │ │
│ │ 4. Verify JWT │
│ │ using JWKS │
│ │ │
│ 5. User is logged in │
│<──────────────────────────────────────────────>│

Step-by-Step Implementation

Step 1: Receive the Token

When the user clicks your app in the Launchpad, they are redirected to your Landing URL with a query parameter:

[Link]

Step 2: Fetch the Public Keys (JWKS)

Fetch the SSO platform's public keys to verify the token signature:

GET {authServiceUrl}/.well-known/[Link]

Response:

{
"keys": [{
"kty": "RSA",
"n": "...",
"e": "AQAB",
"kid": "sso-platform-key-1",
"use": "sig",
"alg": "RS256"
}]
}

Tip: Cache these keys for 5–15 minutes. Do NOT fetch them on every request.

Step 3: Verify the JWT Signature

Verify the RS256 signature using the public key that matches the token's kid (Key ID) header claim.

JavaScript (using SDK):


import { SSOAuth } from '@ap-sso/auth-sdk';

const auth = new SSOAuth({


domain: '[Link]',
clientId: 'not-needed-for-model-1',
authServiceUrl: '[Link]
});

// On your landing page:


const user = await [Link]();
if (user) {
[Link](`Welcome, ${[Link]} (${[Link]})`);
[Link](`Department: ${[Link]}`);
[Link](`Post: ${[Link]}`);
}

JavaScript (without SDK):


async function handleSSOLanding() {
const url = new URL([Link]);
const token = [Link]('sso_token');
if (!token) return null;

// 1. Fetch JWKS
const jwksRes = await fetch('[Link]
const { keys } = await [Link]();

// 2. Parse JWT header


const [headerB64, payloadB64, signatureB64] = [Link]('.');
const header = [Link](atob([Link](/-/g, '+').replace(/_/g, '/')));

// 3. Find matching key


const jwk = [Link](k => [Link] === [Link]) || keys[0];

// 4. Import key and verify


const cryptoKey = await [Link](
'jwk', jwk,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false, ['verify']
);

const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);


const signature = [Link](
atob([Link](/-/g, '+').replace(/_/g, '/')),
c => [Link](0)
);

const valid = await [Link](


'RSASSA-PKCS1-v1_5', cryptoKey, signature, signingInput
);
if (!valid) throw new Error('Invalid token signature');

// 5. Decode payload
const payload = [Link](atob([Link](/-/g, '+').replace(/_/g, '/')));

// 6. Validate claims
if ([Link]() / 1000 > [Link]) throw new Error('Token expired');
if ([Link] !== 'sso_launch') throw new Error('Wrong token type');

// 7. Clean URL
[Link]('sso_token');
[Link]({}, '', [Link]());

return payload; // { sub, cfmsId, name, email, role, deptName, postName, ... }
}

Python (Flask):
import jwt
import requests

JWKS_URL = "[Link]

def verify_sso_token(token: str) -> dict:


"""Verify an SSO launch token and return user identity."""
# Fetch public keys
jwks = [Link](JWKS_URL).json()
public_keys = {}
for key_data in jwks["keys"]:
public_key = [Link].from_jwk(key_data)
public_keys[key_data["kid"]] = public_key

# Find matching key


header = jwt.get_unverified_header(token)
key = public_keys.get(header["kid"])
if not key:
raise ValueError("No matching key found")

# Verify and decode


payload = [Link](
token,
key=key,
algorithms=["RS256"],
issuer="sso-platform",
options={"require": ["exp", "sub", "purpose"]}
)

if [Link]("purpose") != "sso_launch":
raise ValueError("Not an SSO launch token")

return payload

Java (Spring Boot):

import [Link].*;
import [Link];
import [Link].*;
import [Link];

public class SSOTokenVerifier {


private static final String JWKS_URL = "[Link]

public DecodedJWT verifyLaunchToken(String token) throws Exception {


JwkProvider provider = new JwkProviderBuilder(new URL(JWKS_URL))
.cached(10, 24, [Link])
.build();

DecodedJWT unverified = [Link](token);


Jwk jwk = [Link]([Link]());

Algorithm algorithm = Algorithm.RSA256(


([Link]) [Link](), null
);

JWTVerifier verifier = [Link](algorithm)


.withIssuer("sso-platform")
.withClaim("purpose", "sso_launch")
.build();

return [Link](token);
}
}
C# (.NET):

using [Link];
using [Link];
using [Link];
using [Link];

public class SSOTokenVerifier


{
private const string JwksUrl = "[Link]

public async Task<JwtSecurityToken> VerifyLaunchToken(string token)


{
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
JwksUrl, new OpenIdConnectConfigurationRetriever());
var config = await [Link]();

var validationParams = new TokenValidationParameters


{
ValidateIssuer = true,
ValidIssuer = "sso-platform",
ValidateAudience = false,
IssuerSigningKeys = [Link],
ValidAlgorithms = new[] { "RS256" },
};

var handler = new JwtSecurityTokenHandler();


[Link](token, validationParams, out var validatedToken);
return (JwtSecurityToken)validatedToken;
}
}

Step 4: Extract User Identity

The JWT payload contains the user's identity:

{
"sub": "12345",
"cfmsId": "CFMS001234",
"name": "Ravi Kumar",
"email": "[Link]@[Link]",
"deptId": "FIN",
"deptName": "Finance Department",
"postId": "post-7",
"postName": "Deputy Director",
"apcfssDistrictId": "dist-5",
"apcfssDistrictName": "Visakhapatnam",
"purpose": "sso_launch",
"iss": "sso-platform",
"aud": "your-app-slug",
"iat": 1710648000,
"exp": 1710648060
}

Validation Checklist

Check Required How

RS256 signature Yes Verify using JWKS public key

exp claim — token is valid for only 60


Token expiration Yes
seconds

Token purpose Yes purpose must be "sso_launch"


Issuer
Check Yes
Required iss must be "sso-platform"
How

Audience Recommended aud should match your app slug

Model 2 — API Token Exchange

When to Use

Your application is launched from the SSO Launchpad, and you have a backend server that can securely store and use a client secret.

How It Works

┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌────────────┐


│ User │ │ SSO │ │ Your App │ │ SSO Auth │
│ │ │ Launchpad │ │ (Backend) │ │ Service │
└────┬─────┘ └──────┬───────┘ └──────┬──────┘ └─────┬──────┘
│ │ │ │
│ 1. Click app │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ 2. Generate auth │ │
│ │ code (60s TTL) │ │
│ │ │ │
│ 3. Redirect with auth_code │ │
│<─────────────────────────────────────-->│ │
│ [Link] │ │
│ │ │ │
│ │ │ 4. POST /auth/exchange
│ │ │ { code, clientId,│
│ │ │ clientSecret } │
│ │ │───────────────────>│
│ │ │ │
│ │ │ 5. { user, token } │
│ │ │<───────────────────│
│ │ │ │
│ 6. User is logged in │ │
│<────────────────────────────────────────│ │

Step-by-Step Implementation

Step 1: Receive the Auth Code

When the user clicks your app in the Launchpad, they're redirected to your application with a query parameter:

[Link]

Step 2: Exchange the Code (Server-Side)

From your backend server, call the SSO auth service to exchange the code for user identity:

POST {authServiceUrl}/auth/exchange
Content-Type: application/json

{
"code": "Xk9mR2...",
"clientId": "your-client-id",
"clientSecret": "your-client-secret"
}

⚠️ IMPORTANT: This call must be made from your server, not the browser. The clientSecret must never be exposed to the client.
Step 3: Handle the Response

Success Response (200):

{
"success": true,
"data": {
"user": {
"id": "12345",
"cfmsId": "CFMS001234",
"name": "Ravi Kumar",
"email": "[Link]@[Link]",
"role": "department_admin",
"deptId": "FIN",
"deptName": "Finance Department",
"postId": "post-7",
"postName": "Deputy Director",
"apcfssDistrictId": "dist-5",
"apcfssDistrictName": "Visakhapatnam"
},
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"tokenType": "Bearer",
"expiresIn": 900
}
}

Error Responses:

Status Code Cause Fix

400 VALIDATION_ERROR Missing code , clientId , or clientSecret Include all three fields

400 INVALID_CODE Code expired (>60s), used, or invalid Get a new code — codes are single-use

401 INVALID_CLIENT Wrong clientId or clientSecret Verify credentials with SSO team

Step 4: Create a Session for the User

After receiving the user identity, create a session in your application:

Python (Flask):
import requests
from flask import Flask, request, redirect, session

app = Flask(__name__)
AUTH_SERVICE_URL = "[Link]
CLIENT_ID = [Link]["SSO_CLIENT_ID"]
CLIENT_SECRET = [Link]["SSO_CLIENT_SECRET"]

@[Link]("/landing")
def handle_sso_landing():
code = [Link]("auth_code")
if not code:
return "Missing auth code", 400

# Exchange code for user identity (server-to-server)


response = [Link](f"{AUTH_SERVICE_URL}/auth/exchange", json={
"code": code,
"clientId": CLIENT_ID,
"clientSecret": CLIENT_SECRET,
})

if response.status_code != 200:
return f"SSO verification failed: {[Link]()}", 401

data = [Link]()["data"]
user = data["user"]

# Create your own session


session["user_id"] = user["id"]
session["user_name"] = user["name"]
session["user_cfms_id"] = user["cfmsId"]
session["user_role"] = user["role"]
session["user_dept"] = user["deptName"]

return redirect("/dashboard")

Java (Spring Boot):


@RestController
public class SSOLandingController {

@Value("${[Link]-service-url}") private String authServiceUrl;


@Value("${[Link]-id}") private String clientId;
@Value("${[Link]-secret}") private String clientSecret;

@GetMapping("/landing")
public ResponseEntity<?> handleLanding(
@RequestParam("auth_code") String code,
HttpServletRequest request) {

RestTemplate rest = new RestTemplate();


Map<String, String> body = [Link](
"code", code,
"clientId", clientId,
"clientSecret", clientSecret
);

ResponseEntity<Map> response = [Link](


authServiceUrl + "/auth/exchange", body, [Link]
);

Map<String, Object> data = (Map) [Link]().get("data");


Map<String, Object> user = (Map) [Link]("user");

// Create your session


HttpSession session = [Link](true);
[Link]("user", user);

return [Link](302)
.header("Location", "/dashboard")
.build();
}
}

C# ([Link]):
[HttpGet("landing")]
public async Task<IActionResult> HandleLanding([FromQuery(Name = "auth_code")] string code)
{
var client = new HttpClient();
var response = await [Link]($"{_authServiceUrl}/auth/exchange", new
{
code = code,
clientId = _clientId,
clientSecret = _clientSecret
});

var result = await [Link]<ExchangeResponse>();


var user = [Link];

// Create claims and sign in


var claims = new List<Claim>
{
new Claim([Link], [Link]),
new Claim([Link], [Link]),
new Claim("cfmsId", [Link]),
new Claim([Link], [Link]),
};

var identity = new ClaimsIdentity(claims, "SSO");


await [Link](new ClaimsPrincipal(identity));

return Redirect("/dashboard");
}

Alternative: Direct Authentication (Model 2 Variant)

If your app needs to authenticate users directly (not via Launchpad), you can use the /auth/verify endpoint with an API key:

POST {authServiceUrl}/auth/verify
Content-Type: application/json

{
"cfmsId": "CFMS001234",
"password": "user-password",
"apiKey": "sk_live_your_api_key..."
}

This is useful for mobile apps, Postman testing, or backend services that need to authenticate users programmatically.

Model 3 — OAuth2 / OIDC (Recommended)

When to Use

Your application operates independently and needs a "Login with SSO" button. This is the recommended model for new applications.

How It Works

This follows the standard OAuth2 Authorization Code flow with PKCE:
┌──────────┐ ┌─────────────┐ ┌────────────┐
│ User │ │ Your App │ │ SSO Auth │
│ │ │ │ │ Service │
└────┬─────┘ └──────┬──────┘ └─────┬──────┘
│ │ │
│ 1. Click │ │
│ "Login with SSO" │ │
│──────────────────>│ │
│ │ │
│ 2. Redirect to /oauth/authorize │
│<──────────────────────────────────────>│
│ ?client_id=...&redirect_uri=... │
│ │ │
│ 3. User sees SSO login page │
│ (if no SSO session exists) │
│ OR auto-redirects (if session exists)│
│ │ │
│ 4. User logs in (if needed) │
│──────────────────────────────────────->│
│ │ │
│ 5. Redirect back with auth code │
│<──────────────────────────────────────>│
│ /callback?code=ABC&state=XYZ │
│ │ │
│ │ 6. POST /oauth/token│
│ │ { code, verifier } │
│ │───────────────────>│
│ │ │
│ │ 7. { access_token, │
│ │ id_token, │
│ │ refresh_token } │
│ │<───────────────────│
│ │ │
│ │ 8. GET /oauth/userinfo
│ │───────────────────>│
│ │ │
│ │ 9. { user profile }│
│ │<───────────────────│
│ │ │
│ 10. User is │ │
│ logged in │ │
│<──────────────────│ │

Step-by-Step Implementation

Step 1: Generate PKCE Values

PKCE (Proof Key for Code Exchange) is strongly recommended for all clients, and required for SPAs/mobile apps.
// Generate a random code verifier (43-128 characters)
function generateCodeVerifier() {
const array = new Uint8Array(64);
[Link](array);
return [Link](array, b =>
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'[b % 66]
).join('');
}

// Create the code challenge (SHA-256 hash, base64url-encoded)


async function generateCodeChallenge(verifier) {
const hash = await [Link](
'SHA-256', new TextEncoder().encode(verifier)
);
return btoa([Link](...new Uint8Array(hash)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

Step 2: Redirect to Authorize Endpoint

Build the authorization URL and redirect the user:

GET {authServiceUrl}/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=[Link]
&scope=openid profile email roles department
&state=RANDOM_CSRF_STATE
&code_challenge=S256_HASH_OF_VERIFIER
&code_challenge_method=S256

Parameters:

Parameter Required Description

response_type Yes Must be code

client_id Yes Your registered OAuth client ID

Must exactly match a registered redirect


redirect_uri Yes
URI

scope Yes Space-separated list of scopes (see below)

state Yes Random string for CSRF protection

code_challenge Recommended SHA-256 hash of code verifier (base64url)

code_challenge_method Recommended Must be S256

Available Scopes:

Scope Claims Returned

openid sub (user ID)

profile name , cfmsId , preferred_username

email email

role (staff, department_admin,


roles
state_admin)

department deptId , deptName , postId , postName , posts


groups groups (array of group names)
Scope Claims Returned

Step 3: Handle the Callback

After the user authenticates, the SSO service redirects back to your redirect_uri :

[Link]

Validate the state parameter matches what you sent in Step 2 to prevent CSRF attacks.

Step 4: Exchange Code for Tokens

POST {authServiceUrl}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=Xk9mR2...
&redirect_uri=[Link]
&client_id=YOUR_CLIENT_ID
&code_verifier=ORIGINAL_PKCE_VERIFIER

Note: Confidential clients (with backend) can also pass client_secret instead of code_verifier .

Success Response (200):

{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email roles department"
}

Token TTL Purpose

15 Use to call protected APIs (e.g.,


access_token
minutes /oauth/userinfo )

id_token 1 hour Contains user identity claims (JWT)

refresh_token 7 days Use to get new access tokens without re-login

Step 5: Get User Info

Use the access token to fetch the user's profile:

GET {authServiceUrl}/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN

Response:
{
"sub": "12345",
"name": "Ravi Kumar",
"preferred_username": "CFMS001234",
"email": "[Link]@[Link]",
"role": "department_admin",
"dept_id": "FIN",
"dept_name": "Finance Department",
"post_id": "post-7",
"post_name": "Deputy Director",
"apcfss_district_id": "dist-5",
"apcfss_district_name": "Visakhapatnam"
}

Step 6: Refresh Tokens (When Access Token Expires)

When the access token expires (after 15 minutes), use the refresh token:

POST {authServiceUrl}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token (Note: check if supported)


&refresh_token=eyJhbGciOi...
&client_id=YOUR_CLIENT_ID

Complete Implementation Examples

JavaScript (using SDK — Recommended):


import { SSOAuth } from '@ap-sso/auth-sdk';

// Initialize the SDK


const auth = new SSOAuth({
domain: '[Link]',
clientId: 'your-client-id',
redirectUri: '[Link]
scopes: ['openid', 'profile', 'email', 'roles', 'department'],
authServiceUrl: '[Link]
});

// ── Login Page ──────────────────────────────────


// When user clicks "Login with SSO":
[Link]('sso-login-btn').onclick = () => {
[Link](); // Redirects to SSO
};

// ── Callback Page (/auth/callback) ──────────────


// After SSO redirects back:
async function handleCallback() {
try {
const tokens = await [Link]();
[Link]('Login successful!', tokens);

// Get user profile


const user = await [Link]();
[Link](`Welcome, ${[Link]}!`);
[Link](`Department: ${user.dept_name}`);

// Redirect to your app


[Link] = '/dashboard';
} catch (error) {
[Link]('Login failed:', error);
}
}

// ── Check Auth Status ───────────────────────────


if ([Link]()) {
const token = await [Link](); // Auto-refreshes if needed
// Use token for API calls
}

Python (Flask):

import os, secrets, hashlib, base64, requests


from flask import Flask, redirect, request, session, url_for

app = Flask(__name__)
app.secret_key = [Link]["FLASK_SECRET"]

AUTH_URL = [Link]["SSO_AUTH_SERVICE_URL"] # e.g., [Link]


CLIENT_ID = [Link]["SSO_CLIENT_ID"]
REDIRECT_URI = [Link]["SSO_REDIRECT_URI"] # e.g., [Link]

@[Link]("/login")
def login():
# Generate PKCE
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256([Link]()).digest()
).rstrip(b"=").decode()
state = secrets.token_urlsafe(32)

session["pkce_verifier"] = verifier
session["oauth_state"] = state
session["oauth_state"] = state

params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": "openid profile email roles department",
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
}
auth_url = f"{AUTH_URL}/oauth/authorize?" + "&".join(f"{k}={v}" for k, v in [Link]())
return redirect(auth_url)

@[Link]("/auth/callback")
def callback():
# Validate state
if [Link]("state") != [Link]("oauth_state", None):
return "CSRF validation failed", 403

code = [Link]("code")
verifier = [Link]("pkce_verifier")

# Exchange code for tokens


token_res = [Link](f"{AUTH_URL}/oauth/token", data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": verifier,
})

if token_res.status_code != 200:
return f"Token exchange failed: {token_res.text}", 401

tokens = token_res.json()
access_token = tokens["access_token"]

# Fetch user info


user_res = [Link](f"{AUTH_URL}/oauth/userinfo", headers={
"Authorization": f"Bearer {access_token}"
})
user = user_res.json()

# Create session
session["user"] = user
session["access_token"] = access_token
session["refresh_token"] = [Link]("refresh_token")

return redirect("/dashboard")

PHP (Laravel):
// routes/[Link]
Route::get('/login', function () {
$verifier = bin2hex(random_bytes(32));
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$state = bin2hex(random_bytes(16));

session(['pkce_verifier' => $verifier, 'oauth_state' => $state]);

$params = http_build_query([
'response_type' => 'code',
'client_id' => config('sso.client_id'),
'redirect_uri' => config('sso.redirect_uri'),
'scope' => 'openid profile email roles department',
'state' => $state,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
]);

return redirect(config('sso.auth_url') . "/oauth/authorize?{$params}");


});

Route::get('/auth/callback', function (Request $request) {


abort_unless($request->state === session('oauth_state'), 403, 'CSRF mismatch');

$response = Http::asForm()->post(config('sso.auth_url') . '/oauth/token', [


'grant_type' => 'authorization_code',
'code' => $request->code,
'redirect_uri' => config('sso.redirect_uri'),
'client_id' => config('sso.client_id'),
'code_verifier' => session('pkce_verifier'),
]);

$tokens = $response->json();

$userResponse = Http::withToken($tokens['access_token'])
->get(config('sso.auth_url') . '/oauth/userinfo');

$user = $userResponse->json();

// Create local session / find-or-create user


Auth::login(User::firstOrCreate(['sso_id' => $user['sub']], [
'name' => $user['name'],
'email' => $user['email'],
'cfms_id' => $user['preferred_username'],
]));

return redirect('/dashboard');
});

Cross-App SSO (Silent Login)

When a user has already logged in to any SSO-integrated app, subsequent apps skip the login screen entirely:

1. User logs into [Link] via Model 3 → SSO session cookie is set on [Link]
2. User visits [Link] → clicks "Login with SSO" → redirected to [Link]/oauth/authorize
3. SSO sees existing session → automatically redirects back with auth code → no login screen shown
4. [Link] exchanges the code and the user is logged in seamlessly

This is the same behavior as Google's SSO across Gmail, YouTube, Drive, etc.

Using the JavaScript SDK


The JavaScript SDK provides a simple API that handles all the complexity (PKCE, JWKS, token management).
Installation

npm install @ap-sso/auth-sdk

SDK API Reference

import { SSOAuth } from '@ap-sso/auth-sdk';

const auth = new SSOAuth({


domain: '[Link]', // Required
clientId: 'your-client-id', // Required
redirectUri: '/auth/callback', // Default: /callback
scopes: ['openid', 'profile'], // Default: openid profile email
authServiceUrl: '[Link] // Required for Model 1/3
});

Method Model Description

[Link]() 1 Verify JWT from URL ?sso_token=...

[Link](code, clientId, secret) 2 Exchange auth code for user identity

[Link]() 3 Redirect to SSO login page

Handle OAuth callback, exchange code for


[Link]() 3
tokens

[Link]() 3 Fetch user profile from /oauth/userinfo

[Link]() All Check if user has valid tokens

[Link]() All Get current access token (auto-refreshes)

[Link]() All Clear tokens and redirect to logout

User Data Reference


All models return user data with these fields:

Field Type Description Example

sub / id string Unique user ID in the platform "12345"

CFMS ID (government employee


cfmsId string "CFMS001234"
ID)

name string Full name "Ravi Kumar"

string |
email Email address "ravi@[Link]"
null

role string Platform role "staff" , "department_admin" , "state_admin"

string |
deptId Department code "FIN"
null

string |
deptName Department name "Finance Department"
null

string |
postId Post/designation ID "post-7"
null

string | Post/designation name


postName null "Deputy Director"
Field Type Description Example
posts array All posts held by the user [{postId, postName, deptId, deptName}]

string |
apcfssDistrictId APCFSS district ID "dist-5"
null

string |
apcfssDistrictName APCFSS district name "Visakhapatnam"
null

OIDC Discovery & JWKS


The platform provides standard OIDC discovery:

Discovery Document:

GET {authServiceUrl}/.well-known/openid-configuration

{
"issuer": "sso-platform",
"authorization_endpoint": "{base}/oauth/authorize",
"token_endpoint": "{base}/oauth/token",
"userinfo_endpoint": "{base}/oauth/userinfo",
"jwks_uri": "{base}/.well-known/[Link]",
"scopes_supported": ["openid", "profile", "email", "roles", "department", "groups"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]
}

JWKS (Public Keys):

GET {authServiceUrl}/.well-known/[Link]

Security Best Practices

MUST Do

Practice Details

Verify JWT signatures Always verify RS256 signatures using JWKS (Model 1)

Keep secrets server-


clientSecret and API keys must never be in frontend code
side

Always use PKCE for OAuth2 flows, especially for SPAs and mobile
Use PKCE
apps

Validate state Compare the state returned in the callback with what you sent

Check token expiration Always check exp claim before trusting any token

Use HTTPS All production communication must use HTTPS

Cache JWKS Fetch JWKS every 5–15 minutes, not on every request

Use httpOnly cookies Store tokens in httpOnly cookies, not localStorage

MUST NOT Do
Anti-Pattern Risk

Token forgery — anyone could create fake


Skip signature verification
tokens

Store tokens in localStorage XSS attacks can steal tokens

Expose clientSecret in frontend Anyone can impersonate your application

Reuse auth codes Codes are single-use — replay attacks

Hardcode credentials Credentials leak through version control

Trust tokens without expiry


Expired tokens may contain stale data
check

Troubleshooting

Problem Cause Solution

Auth code expired or already Codes expire in 60s (Model 1/2) or 2 min (Model 3) and are single-
INVALID_CODE
used use

INVALID_CLIENT Wrong client credentials Verify clientId and clientSecret with the SSO team

JWT signature fails JWKS not fetched or stale Refresh JWKS from /.well-known/[Link]

State mismatch CSRF protection triggered Ensure state sent matches the one returned in callback

redirect_uri not registered Callback URL doesn't match Your redirect URI must exactly match what's registered

Token expired TTL exceeded Launch tokens: 60s, access tokens: 15min, refresh: 7 days

CORS error Your origin not allowed Contact SSO team to add your domain to allowed origins

Login screen shows despite


SSO session expired Session cookies expire. User needs to re-authenticate once
SSO

API Reference

Authentication Endpoints

Method Endpoint Auth Description

POST /auth/login None Login with username/password

POST /auth/refresh None Refresh access token

POST /auth/logout Bearer Logout & revoke session

GET /auth/me Bearer Get current user profile

Direct auth with API key (Model 2


POST /auth/verify None
variant)

Model 1 — JWT-as-Identity

Method Endpoint Auth Description

POST Bearer Generate signed launch JWT


/auth/launch-token (internal/launchpad)
Method Endpoint Auth Description

Model 2 — API Token Exchange

Method Endpoint Auth Description

POST /auth/generate-code Bearer Generate auth code (internal/launchpad)

POST /auth/exchange None Exchange auth code for user identity

Model 3 — OAuth2/OIDC

Method Endpoint Auth Description

GET /oauth/authorize None Start OAuth2 authorization flow

POST /oauth/token None Exchange authorization code for tokens

GET /oauth/userinfo Bearer Fetch user profile

Discovery

Method Endpoint Description

GET /.well-known/openid-configuration OIDC discovery document

Public keys for JWT verification


GET /.well-known/[Link]
(RS256)

Testing with Postman

Test Model 2 (API Exchange)

1. Login to get a platform access token:

POST {authServiceUrl}/auth/login
Body: { "username": "testuser", "password": "testpass" }
→ Copy accessToken from response

2. Generate an auth code (simulating launchpad):

POST {authServiceUrl}/auth/generate-code
Headers: Authorization: Bearer <accessToken>
Body: { "applicationId": "your-app-uuid" }
→ Copy code from response

3. Exchange the code:

POST {authServiceUrl}/auth/exchange
Body: {
"code": "<code from step 2>",
"clientId": "your-client-id",
"clientSecret": "your-client-secret"
}
→ Returns user identity + access token

Test Model 2 (Direct Verify)


POST {authServiceUrl}/auth/verify
Body: {
"cfmsId": "CFMS001234",
"password": "user-password",
"apiKey": "sk_live_your_api_key"
}
→ Returns user identity + access token

Test Model 3 (OAuth2)

1. Open in browser:

{authServiceUrl}/oauth/authorize?response_type=code&client_id=YOUR_ID&redirect_uri=YOUR_CALLBACK&scope=openid+profile+email&state=test

2. Login on the SSO page, get redirected back with ?code=...

3. Exchange the code:

POST {authServiceUrl}/oauth/token
Content-Type: application/x-www-form-urlencoded
Body: grant_type=authorization_code&code=<CODE>&redirect_uri=YOUR_CALLBACK&client_id=YOUR_ID&client_secret=YOUR_SECRET

Support
For integration support, credential provisioning, or to register your application:

Documentation: This guide + OIDC Discovery at {authServiceUrl}/.well-known/openid-configuration


SDKs: Available for JavaScript, Python, PHP, Java/Kotlin, Swift, and C#

You might also like