| name | moai-security-identity |
| version | 4.0.0 |
| status | stable |
| description | Enterprise Skill for advanced development |
| allowed-tools | Read, Bash, WebSearch, WebFetch |
moai-security-identity: SAML 2.0 & OIDC Identity Management
Enterprise SSO with SAML 2.0, OpenID Connect & OAuth 2.0
Trust Score: 9.9/10 | Version: 4.0.0 | Enterprise Mode | Last Updated: 2025-11-12
Overview
Identity and Access Management (IAM) for enterprise applications using SAML 2.0 for legacy systems and OpenID Connect (OIDC) for modern APIs. 2025 trend: 72% of enterprises now adopt multi-protocol SSO. This Skill covers SAML assertion validation, OIDC token processing, JWT verification, JIT provisioning, and SCIM 2.0 user synchronization.
When to use this Skill:
- Implementing enterprise Single Sign-On (SSO)
- Supporting multiple identity protocols (SAML + OIDC)
- Integrating Auth0, Keycloak, or Okta
- SAML/OIDC federation between organizations
- User provisioning automation (SCIM)
- Legacy B2B SAML + modern API OIDC
Level 1: Foundations
SAML vs OIDC Comparison
SAML 2.0 (XML-based, legacy enterprise):
├─ Protocol: XML assertions over HTTP POST/Redirect
├─ Use: Legacy web applications, B2B federation
├─ Complexity: Higher (XML parsing, certificates)
├─ Token Format: SAML Assertions (XML)
└─ Adoption: Enterprise (Salesforce, SharePoint, SAP)
OIDC (JSON-based, modern APIs):
├─ Protocol: Built on OAuth 2.0, REST APIs
├─ Use: Modern web/mobile apps, microservices
├─ Complexity: Lower (JSON, standard OAuth)
├─ Token Format: JWT (JSON Web Tokens)
└─ Adoption: Modern (mobile, SPA, APIs)
Best Practice (2025):
- Legacy B2B apps: SAML 2.0
- Modern APIs: OIDC
- Hybrid enterprises: Both (via federation)
SAML 2.0 Flow
1. User clicks "Login with Company SSO"
↓
2. Service Provider (SP) → Identity Provider (IdP)
Sends: AuthnRequest (signed, encrypted)
↓
3. User authenticates at IdP (username/password)
↓
4. IdP → Service Provider (SAML Response)
Contains: SAML Assertion (signed, encrypted)
├─ NameID (user identifier)
├─ Attributes (email, groups, roles)
└─ AuthnStatement (authentication confirmation)
↓
5. SP verifies signature, creates session
↓
6. User logged in to SP
OIDC Flow
1. User clicks "Login with Google"
↓
2. SPA → Authorization Server
Sends: authorization request (client_id, redirect_uri)
↓
3. User authenticates at Authorization Server
↓
4. Authorization Server → SPA (authorization code)
↓
5. SPA backend → Authorization Server (token exchange)
Sends: authorization code, client_secret
↓
6. Authorization Server → SPA backend
Returns: ID Token (JWT), Access Token, Refresh Token
↓
7. SPA backend creates session, user logged in
Level 2: Implementation Patterns
Pattern 1: SAML 2.0 Assertion Validation
const passport = require('passport');
const { Strategy } = require('@node-saml/passport-saml');
const fs = require('fs');
const samlStrategy = new Strategy(
{
// Service Provider (our app) metadata
entryPoint: 'https://idp.example.com/sso', // IdP's SSO endpoint
issuer: 'https://ourapp.com',
callbackURL: 'https://ourapp.com/auth/saml/callback',
// Certificates for signature verification
cert: fs.readFileSync('./certs/idp-public.pem', 'utf-8'),
// Security settings
validateInResponseTo: true,
wantAssertionsSigned: true, // Require signed assertions
wantAuthnResponseSigned: true, // Require signed response
// Encryption
decryptionPvk: fs.readFileSync('./certs/sp-private.pem', 'utf-8'),
// Identifier format
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
},
(profile, done) => {
// profile contains:
// - nameID: unique user identifier
// - nameIDFormat: format of identifier
// - sessionIndex: session index
// - attributes: user attributes from IdP
console.log('SAML Profile:', profile);
// Find or create user
const user = {
id: profile.nameID,
email: profile.attributes.email,
name: profile.attributes.displayName,
groups: profile.attributes.groups || [],
};
done(null, user);
}
);
passport.use('saml', samlStrategy);
// Express routes
app.get('/auth/saml', passport.authenticate('saml', {
failureRedirect: '/login',
}));
app.post('/auth/saml/callback', (req, res, next) => {
passport.authenticate('saml', (err, user) => {
if (err || !user) {
return res.redirect('/login?error=authentication_failed');
}
// Create session
req.logIn(user, (err) => {
if (err) return next(err);
res.redirect('/dashboard');
});
})(req, res, next);
});
app.get('/auth/saml/metadata', (req, res) => {
// Provide SP metadata for IdP to consume
const metadata = samlStrategy.generateServiceProviderMetadata(
null, // decryption certificate
null // encryption certificate
);
res.type('application/xml').send(metadata);
});
// Logout (SAML SLO)
app.get('/logout', (req, res) => {
if (!req.user) {
return res.redirect('/');
}
const options = {
destination: 'https://idp.example.com/slo', // IdP's SLO endpoint
issuer: 'https://ourapp.com',
};
samlStrategy.logout(req, (err, url) => {
if (err) return res.status(500).send(err);
req.logOut((err) => {
if (err) return res.status(500).send(err);
res.redirect(url);
});
});
});
Pattern 2: OIDC Token Validation
const { Issuer } = require('openid-client');
const jwt = require('jsonwebtoken');
class OIDCValidator {
constructor(config) {
this.config = config;
this.issuer = null;
this.client = null;
this.jwks = null;
}
async initialize() {
// Discover OIDC provider configuration
this.issuer = await Issuer.discover(this.config.issuerUrl);
// Create client
this.client = new this.issuer.Client({
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uris: [this.config.redirectUri],
response_types: ['code'],
});
// Cache JWKS (JSON Web Key Set)
const response = await fetch(`${this.config.issuerUrl}/.well-known/jwks.json`);
this.jwks = await response.json();
}
// Validate ID Token signature
validateIdToken(idToken) {
const decoded = jwt.decode(idToken, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
const { header, payload } = decoded;
// 1. Find public key matching kid
const jwk = this.jwks.keys.find(key => key.kid === header.kid);
if (!jwk) {
throw new Error('Key not found in JWKS');
}
// 2. Convert JWK to PEM
const publicKey = this.jwkToPem(jwk);
// 3. Verify signature
try {
const verified = jwt.verify(idToken, publicKey, {
algorithms: ['RS256'], // Ensure RS256
issuer: this.config.issuerUrl,
audience: this.config.clientId,
});
return verified;
} catch (error) {
throw new Error(`Token verification failed: ${error.message}`);
}
}
// Validate Access Token
validateAccessToken(accessToken) {
const decoded = jwt.decode(accessToken, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
// 1. Check expiration
const { payload } = decoded;
const now = Math.floor(Date.now() / 1000);
if (payload.exp <= now) {
throw new Error('Token expired');
}
// 2. Check issuer
if (payload.iss !== this.config.issuerUrl) {
throw new Error('Invalid issuer');
}
return payload;
}
// Convert JWK to PEM format
jwkToPem(jwk) {
// Implementation using node-jose or similar
// Returns PEM-formatted public key
// ... (crypto conversion logic)
}
}
// Usage
const oidcValidator = new OIDCValidator({
issuerUrl: 'https://auth.example.com',
clientId: 'my-app-id',
clientSecret: 'my-secret',
redirectUri: 'https://myapp.com/auth/callback',
});
await oidcValidator.initialize();
// In middleware
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Missing authorization' });
}
const token = authHeader.replace('Bearer ', '');
try {
req.user = oidcValidator.validateIdToken(token);
next();
} catch (error) {
res.status(401).json({ error: error.message });
}
});
Pattern 3: SCIM 2.0 User Provisioning
class SCIMUserProvisioner {
constructor(config) {
this.config = config;
}
// Handle SCIM provisioning webhook from IdP
handleScimWebhook(scimEvent) {
switch (scimEvent.resourceType) {
case 'User':
return this.handleUserEvent(scimEvent);
case 'Group':
return this.handleGroupEvent(scimEvent);
default:
throw new Error(`Unknown resource type: ${scimEvent.resourceType}`);
}
}
async handleUserEvent(event) {
const { externalId, attributes } = event;
switch (event.eventType) {
case 'user.created':
return this.createUser(attributes);
case 'user.updated':
return this.updateUser(externalId, attributes);
case 'user.deleted':
return this.deleteUser(externalId);
default:
throw new Error(`Unknown event: ${event.eventType}`);
}
}
async createUser(attributes) {
// Validate required fields
if (!attributes.email || !attributes.userName) {
throw new Error('Missing required fields');
}
// Create database record
const user = await db.users.create({
externalId: attributes.externalId,
email: attributes.email,
displayName: attributes.displayName,
givenName: attributes.givenName,
familyName: attributes.familyName,
active: attributes.active ?? true,
groups: attributes.groups || [],
});
return user;
}
async updateUser(externalId, attributes) {
const user = await db.users.findByExternalId(externalId);
if (!user) {
throw new Error(`User not found: ${externalId}`);
}
// Update fields
const updated = await db.users.update(user.id, {
displayName: attributes.displayName,
active: attributes.active,
groups: attributes.groups || [],
});
return updated;
}
async deleteUser(externalId) {
const user = await db.users.findByExternalId(externalId);
if (!user) {
throw new Error(`User not found: ${externalId}`);
}
// Soft delete (mark as inactive)
await db.users.update(user.id, { active: false });
return { success: true };
}
async handleGroupEvent(event) {
// Similar pattern for group provisioning
// Handle group.created, group.updated, group.deleted
}
}
// Express endpoint for SCIM webhooks
app.post('/scim/webhook', async (req, res) => {
try {
// Verify webhook signature
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const provisioner = new SCIMUserProvisioner(config);
const result = await provisioner.handleScimWebhook(req.body);
res.json(result);
} catch (error) {
console.error('SCIM webhook error:', error);
res.status(400).json({ error: error.message });
}
});
Pattern 4: JWT Bearer Token in APIs
class JWTMiddleware {
constructor(publicKey) {
this.publicKey = publicKey;
}
middleware() {
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = authHeader.slice(7); // Remove "Bearer "
try {
const payload = jwt.verify(token, this.publicKey, {
algorithms: ['RS256'],
});
req.user = {
id: payload.sub,
email: payload.email,
scope: payload.scope ? payload.scope.split(' ') : [],
};
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
}
}
// Usage
const jwtMiddleware = new JWTMiddleware(publicKey);
app.use('/api', jwtMiddleware.middleware());
app.get('/api/protected', (req, res) => {
res.json({
message: `Hello, ${req.user.email}`,
scopes: req.user.scope,
});
});
Level 3: Advanced
Advanced: Context7 MCP Integration
const { Context7Client } = require('context7-mcp');
class IdentityThreatIntelligence {
constructor(apiKey) {
this.context7 = new Context7Client(apiKey);
}
// Check user identity against threat intelligence
async validateUserIdentity(user) {
const threats = await this.context7.query({
type: 'identity_threat',
email: user.email,
externalId: user.externalId,
tags: ['fraud', 'compromise', 'insider_threat'],
});
return {
safe: threats.severity === 0,
severity: threats.severity,
details: threats,
};
}
// Monitor provisioning events for anomalies
async analyzeProvisioningEvent(event) {
const analysis = await this.context7.query({
type: 'provisioning_anomaly',
eventType: event.eventType,
timestamp: event.timestamp,
userId: event.externalId,
});
if (analysis.anomalous) {
console.warn('Anomalous provisioning event detected:', analysis);
// Alert security team
}
return analysis;
}
}
Checklist
- SAML 2.0 strategy configured with certificate verification
- OIDC provider discovery working
- JWT token signature validation implemented
- Token expiration checks in place
- SCIM webhook handling for user provisioning
- JIT (Just-In-Time) provisioning working
- Multi-protocol SSO (SAML + OIDC) tested
- Identity threat intelligence integrated
- SSO logout (SLO) working
- Performance tested at scale
Quick Reference
| Feature | Implementation |
|---|---|
| SAML | @node-saml/passport-saml 3.2.4+ |
| OIDC | openid-client (npm) |
| JWT | jsonwebtoken (npm) |
| SCIM | Custom webhook handler |
| Monitoring | Context7 MCP |