Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/config/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export async function initializeDataSource(): Promise<void> {
}
} catch (error) {
logger.error("Failed to initialize DataSource", { error });
throw new Error(`DataSource initialization failed: ${error instanceof Error ? error.message : String(error)}`);
throw new Error(
`DataSource initialization failed: ${error instanceof Error ? error.message : String(error)}`
);
}
}
75 changes: 61 additions & 14 deletions src/routes/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Router, type NextFunction, type Request, type RequestHandler, type Response } from "express";
import {
Router,
type NextFunction,
type Request,
type RequestHandler,
type Response,
} from "express";
import Joi from "joi";
import { createAuthController } from "../controllers/auth.controller";
import { createAuthMiddleware } from "../middleware/auth.middleware";
Expand All @@ -7,24 +13,60 @@ import { createAuthRateLimitMiddleware } from "../middleware/rate-limit.middlewa
import type { AuthService } from "../services/auth.service";
import type { AppLogger } from "../observability/logger";

// Strict schemas: enforce Stellar G... format hint, length bounds, and sanitized inputs.
const STELLAR_PUBLIC_KEY_PATTERN = /^G[A-Z2-7]{55}$/;
const NONCE_PATTERN = /^[A-Za-z0-9:_-]+$/;
const SIGNATURE_PATTERN = /^[A-Za-z0-9+/=_:.-]+$/;

type AsyncRouteHandler = (req: Request, res: Response, next: NextFunction) => Promise<void> | void;

const publicKeySchema = Joi.string().trim().required();
const publicKeySchema = Joi.string()
.trim()
.length(56)
.pattern(STELLAR_PUBLIC_KEY_PATTERN)
.messages({
"string.length": "publicKey must be 56 characters long",
"string.pattern.base": "publicKey must be a valid Stellar public key",
})
.required();

const challengeSchema = Joi.object({
publicKey: publicKeySchema,
}).unknown(true);
})
.unknown(false)
.options({ abortEarly: false, convert: true, stripUnknown: true });

const verifySchema = Joi.object({
publicKey: publicKeySchema,
nonce: Joi.string().trim().required(),
signature: Joi.string().trim().required(),
}).unknown(true);
nonce: Joi.string()
.trim()
.min(16)
.max(256)
.pattern(NONCE_PATTERN)
.messages({
"string.pattern.base": "nonce contains unsupported characters",
})
.required(),
signature: Joi.string()
.trim()
// Encoding shape is validated here; cryptographic byte length remains an
// authentication concern in AuthService so malformed signatures continue
// to return 401 instead of changing the public contract to a 400.
.min(2)
.max(512)
.pattern(SIGNATURE_PATTERN)
.messages({
"string.pattern.base": "signature contains unsupported characters",
})
.required(),
})
.unknown(false)
.options({ abortEarly: false, convert: true, stripUnknown: true });

function wrapAuthHandler(
routeName: string,
handler: AsyncRouteHandler,
logger: AppLogger,
logger: AppLogger
): RequestHandler {
return async (req, res, next) => {
try {
Expand Down Expand Up @@ -62,30 +104,35 @@ export function createAuthRouter(authService: AuthService, logger: AppLogger): R
const authMiddleware = createAuthMiddleware(authService);
// `/challenge` and `/verify` are unauthenticated by design: the wallet
// signature is the auth check, so they need their own abuse protection.
const authRateLimiter = createAuthRateLimitMiddleware(logger);
// Keep challenge generation and signature verification in independent
// buckets. Sharing one limiter allowed repeated challenge requests to
// consume the verification budget and lock a caller out of completing an
// otherwise valid login flow.
const challengeRateLimiter = createAuthRateLimitMiddleware(logger);
const verifyRateLimiter = createAuthRateLimitMiddleware(logger);

router.use(markAuthRouteBase());
router.use(noStoreAuthResponse());

router.post(
"/challenge",
authRateLimiter,
challengeRateLimiter,
validateBody(challengeSchema),
wrapAuthHandler("auth.challenge", controller.challenge as AsyncRouteHandler, logger),
wrapAuthHandler("auth.challenge", controller.challenge as AsyncRouteHandler, logger)
);

router.post(
"/verify",
authRateLimiter,
verifyRateLimiter,
validateBody(verifySchema),
wrapAuthHandler("auth.verify", controller.verify as AsyncRouteHandler, logger),
wrapAuthHandler("auth.verify", controller.verify as AsyncRouteHandler, logger)
);

router.get(
"/me",
authMiddleware,
wrapAuthHandler("auth.me", controller.me as AsyncRouteHandler, logger),
wrapAuthHandler("auth.me", controller.me as AsyncRouteHandler, logger)
);

return router;
}
}
Loading