Testing Finding
Testing Finding
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.
3 OAuth2/OIDC Your App ↔ Auth Server Standalone apps with "Login with SSO" button
Prerequisites
Before integrating, contact the SSO Platform team to register your application. You will receive:
Client Secret OAuth2 client secret — keep this server-side only! Model 2
Model 2
API Key Alternative to Client Secret for Model 2
(optional)
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]
Python (pip)
// [Link]
repositories {
mavenCentral()
maven {
url = uri("[Link]
isAllowInsecureProtocol = true // Remove when using HTTPS
}
}
dependencies {
implementation("[Link]:auth-kotlin:1.0.0")
}
C# / .NET (NuGet)
PHP (Composer)
The PHP SDK is distributed as a zip archive from Nexus. Download and extract:
require_once 'vendor/apcfss/sso-auth/src/[Link]';
Swift (SPM)
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.
Nexus
Language Package Name Status
Repository
3. Environment Setup
# 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]
┌──────────────────────────────────┐
│ 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)
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
When the user clicks your app in the Launchpad, they are redirected to your Landing URL with a query parameter:
[Link]
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.
Verify the RS256 signature using the public key that matches the token's kid (Key ID) header claim.
// 1. Fetch JWKS
const jwksRes = await fetch('[Link]
const { keys } = await [Link]();
// 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]
if [Link]("purpose") != "sso_launch":
raise ValueError("Not an SSO launch token")
return payload
import [Link].*;
import [Link];
import [Link].*;
import [Link];
return [Link](token);
}
}
C# (.NET):
using [Link];
using [Link];
using [Link];
using [Link];
{
"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
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
Step-by-Step Implementation
When the user clicks your app in the Launchpad, they're redirected to your application with a query parameter:
[Link]
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": 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:
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
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
if response.status_code != 200:
return f"SSO verification failed: {[Link]()}", 401
data = [Link]()["data"]
user = data["user"]
return redirect("/dashboard")
@GetMapping("/landing")
public ResponseEntity<?> handleLanding(
@RequestParam("auth_code") String code,
HttpServletRequest request) {
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
});
return Redirect("/dashboard");
}
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.
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
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('');
}
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:
Available Scopes:
email email
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.
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 .
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email roles department"
}
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"
}
When the access token expires (after 15 minutes), use the refresh token:
POST {authServiceUrl}/oauth/token
Content-Type: application/x-www-form-urlencoded
Python (Flask):
app = Flask(__name__)
app.secret_key = [Link]["FLASK_SECRET"]
@[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")
if token_res.status_code != 200:
return f"Token exchange failed: {token_res.text}", 401
tokens = token_res.json()
access_token = tokens["access_token"]
# 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));
$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',
]);
$tokens = $response->json();
$userResponse = Http::withToken($tokens['access_token'])
->get(config('sso.auth_url') . '/oauth/userinfo');
$user = $userResponse->json();
return redirect('/dashboard');
});
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.
string |
email Email address "ravi@[Link]"
null
string |
deptId Department code "FIN"
null
string |
deptName Department name "Finance Department"
null
string |
postId Post/designation ID "post-7"
null
string |
apcfssDistrictId APCFSS district ID "dist-5"
null
string |
apcfssDistrictName APCFSS district name "Visakhapatnam"
null
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"]
}
GET {authServiceUrl}/.well-known/[Link]
MUST Do
Practice Details
Verify JWT signatures Always verify RS256 signatures using JWKS (Model 1)
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
Cache JWKS Fetch JWKS every 5–15 minutes, not on every request
MUST NOT Do
Anti-Pattern Risk
Troubleshooting
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
API Reference
Authentication Endpoints
Model 1 — JWT-as-Identity
Model 3 — OAuth2/OIDC
Discovery
POST {authServiceUrl}/auth/login
Body: { "username": "testuser", "password": "testpass" }
→ Copy accessToken from response
POST {authServiceUrl}/auth/generate-code
Headers: Authorization: Bearer <accessToken>
Body: { "applicationId": "your-app-uuid" }
→ Copy code from response
POST {authServiceUrl}/auth/exchange
Body: {
"code": "<code from step 2>",
"clientId": "your-client-id",
"clientSecret": "your-client-secret"
}
→ Returns user identity + access token
1. Open in browser:
{authServiceUrl}/oauth/authorize?response_type=code&client_id=YOUR_ID&redirect_uri=YOUR_CALLBACK&scope=openid+profile+email&state=test
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: