This guide presenets practical example to implement custom authentication in SAP CAP (Node.js). I hav
The key to enabling custom authentication in CAP is the auth configuration in package.json. This tells the CAP runtime to use your custom middleware handler for all incoming requests, allowing you to control the identity verification process.
{
"cds": {
"requires": {
"auth": {
"impl": "auth/basic-auth.ts"
}
}
}
}When handling passwords, security must be the top priority. The following code demonstrates secure handling, specifically addressing the timing attack vector, which is often overlooked.
// auth/basic-auth.ts
import crypto from 'crypto';
const PASSWORD_PEPPER = process.env.PASSWORD_PEPPER || "";
// Timing-safe comparison prevents timing attacks
function secureCompare(a: string, b: string): boolean {
// Ensure we compare strings of the same length
if (a.length !== b.length) return false;
// crypto.timingSafeEqual is CRITICAL for password security
return crypto.timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
}
function hashPassword(password: string): string {
if (!password) {
throw new Error("Password cannot be empty");
}
// NOTE: SHA-256 is used here ONLY for simplicity in a Basic Auth example.
// DO NOT use SHA-256 in production. Use bcrypt or argon2.
return crypto
.createHash('sha256')
.update(password + PASSWORD_PEPPER)
.digest('hex');
}crypto.timingSafeEqual(): Guarantees constant-time comparison, preventing attackers from using timing measurements to guess password characters.
Password Pepper: An extra layer of security beyond salting.
Production Note: In a production system, always use bcrypt or argon2 instead of simple hash functions.
The user schema must be designed to support both local password-based authentication (for Basic Auth) and future external OAuth integrations.
// db/schema.cds
entity Users {
key ID : UUID;
email : String(255) @assert.unique;
username : String(100) @assert.unique;
passwordHash: String(255);
firstName : String(100);
lastName : String(100);
provider : String(50); // 'local', 'google', 'oidc'
providerId : String(255); // ID from external provider (for OAuth)
isActive : Boolean default true;
}This core function handles credential verification against the database.
const credentialsAuthentication = async ({
email,
password
}: {
email: string,
password: string
}) => {
const { Users } = cds.entities;
try {
// 1. Validate Input
if (!email || !password) {
return { success: false, message: "Email and password are required", user: null };
}
// Normalize email for consistent lookups
const normalizedEmail = email.trim().toLowerCase();
// 2. Query User
const user = await SELECT.one
.from(Users)
.where({
email: normalizedEmail,
isActive: true
});
// 3. Security Check: Prevent User Enumeration Attack
// Use the same error message whether the user is missing or the password is wrong.
if (!user) {
return { success: false, message: "Invalid credentials", user: null };
}
// 4. Secure Password Verification
const passwordHash = hashPassword(password);
const isPasswordValid = secureCompare(user.passwordHash, passwordHash);
if (!isPasswordValid) {
return { success: false, message: "Invalid credentials", user: null };
}
return {
success: true,
message: 'Login successful',
user: {
id: user.ID,
email: user.email,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
}
};
} catch (err) {
cds.log('auth').error('Authentication error:', err);
return { success: false, message: "Authentication failed", user: null };
}
}Preventing User Enumeration: Using the generic "Invalid credentials" message.
Email Normalization: Ensures query consistency.
The authentication handler is implemented as Express Middleware. It parses the Basic Auth header and creates the essential CAP-compatible user object.
import { Request, Response, NextFunction } from 'express';
import cds from '@sap/cds';
// ... import credentialsAuthentication ... (from section 3)
export default async function custom_auth(
req: Request,
res: Response,
next: NextFunction
) {
try {
const authHeader = req.headers.authorization;
// Allow requests without Basic Auth header to proceed.
if (!authHeader || !authHeader.startsWith('Basic ')) {
return next();
}
// Parse Basic Auth header
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer
.from(base64Credentials, 'base64')
.toString('utf-8');
const [email, password] = credentials.split(':', 2);
if (!email?.trim() || !password) {
res.status(401).json({ error: 'Invalid credentials format' });
return;
}
const auth = await credentialsAuthentication({ email, password });
const { success, user } = auth;
if (success && user) {
// CRITICAL: Create the CAP-compatible user object
const roles = user.roles.length > 0 ? user.roles : ['authenticated'];
const capUser = new cds.User({
id: user.id,
attr: {
email: user.email,
username: user.username,
firstName: user.firstName,
lastName: user.lastName
}
});
// Set the user context in both CAP and Express
cds.context.user = capUser;
req.user = capUser;
next();
} else {
res.status(401).json({ error: auth.message });
}
} catch (error) {
cds.log('auth').error('Unexpected authentication error:', error);
res.status(401).json({ error: 'Unauthorized' });
}
}The user context must be set in both cds.context.user (for CAP Services) and req.user (for Express-based middleware).
With the custom handler successfully establishing the user context, you can immediately leverage CAP's built-in authorization features:
// srv/test-service.cds
service TestService @(requires: 'authenticated-user') {
entity Products as projection on template.Products;
}
# 1. Base64 encode your credentials (email:password)
echo -n 'john.doe@example.com:password' | base64
# 2. Test the endpoint using the encoded value in the Authorization header
curl -X GET http://localhost:4004/odata/v4/test/Products \
-H "Authorization: Basic am9obi5kb2VAZXhhbXBsZS5jb206cGFzc3dvcmQ="
Expected: 200 OK with product data.
# Test without the Authorization header
curl -X GET http://localhost:4004/odata/v4/test/ProductsExpected: 401 Unauthorized.
It's crucial to acknowledge that using XSUAA, IAS (Identity Authentication Service), or standard JWT-based authentication is the recommended and preferred approach for the vast majority of SAP CAP applications. These services offer enterprise-grade security, compliance, and integration with the SAP ecosystem.
However, the SAP CAP intentionally provides mechanisms for custom authentication. This approach is necessary when:
B2C Applications: Requiring fully custom registration and login user experiences.
Legacy System Migration: Integrating existing applications with established user databases and authentication logic.
If your use case aligns with standard enterprise authentication, you should prioritize XSUAA or IAS. Custom authentication should be a deliberate, technically driven choice.
This guide provides the robust foundation for custom Basic Authentication in SAP CAP. Stay tuned for the next parts of this series, where we will extend this implementation.
The journey continues!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.