accredifysg/singpass-login
Laravel package for SingPass Login, MyInfo, and CorpPass with a FAPI 2.0–style flow: PAR, DPoP, PKCE, and private-key JWT client assertions. Shared services handle discovery, token exchange, and JWE/JWS processing, with thin per-provider controllers.
Architecture fit. The package is structured the way a Laravel TPM would want an identity integration to be structured: a shared FAPI 2.0 layer (FapiAuthenticationService, FapiCallbackService, DPoPService, PARService, TokenExchangeService, JwtService, JwksService, OpenIdDiscoveryService, GetUserInfoService, PKCEService) with thin per-provider controllers for SingPass Login, MyInfo, and CorpPass. That means SingPass, MyInfo, and CorpPass share one hardened choreography instead of three drifting implementations, which materially reduces the security-review surface. The package is event-driven (SingPassSuccessfulLoginEvent, MyInfoDataRetrievedEvent, CorpPassSuccessfulLoginEvent, CorpPassDataRetrievedEvent) with an optional publishable default listener, and everything is env/config-driven across four published config files (ndi.php, singpass-login.php, myinfo.php, corppass-login.php). Modules can be toggled independently, and route paths are configurable, so partial adoption is viable.
Integration feasibility. Mechanically low-friction: composer require, publish config, populate env, decide whether to use the default listener or a custom one. Frontend contract is explicit and modern — auth controllers return { "redirect_url": ... } and expect the browser/SPA to call them with credentials: 'same-origin', then navigate. The /ndi/jwks endpoint and the *_controller config overrides give clean escape hatches. The real work is not code — it's provisioning: SingPass/MyInfo/CorpPass client IDs and redirect URIs, a private JWKS registered with the IdP, and correct SINGPASS_DOMAIN / discovery endpoints per environment.
Technical risk. Four clusters:
NDI_PRIVATE_JWKS in env implies a long-lived client assertion private key in application config. That is workable but must be reconciled with your secret-management standard (KMS/Secrets Manager, rotation, per-environment separation). DPoP keys appear ephemeral, which is better.state validation imply cache/session backends. On multi-node deployments, a file or array cache silently breaks PAR/callback correlation.Key questions. What Laravel and PHP versions are supported and tested (11/12, 8.2+)? Is the CI badge real — PHPStan max and coverage are advertised, but is there a runnable test suite and a security disclosure policy? Is the private JWKS expected to be a single static key, and is rotation documented? Where is discovery cached and is that cache driver-agnostic? Does the package ship any persistence/audit layer for MyInfo data, or is retention entirely my responsibility under PDPA? What is the support/escalation path if SingPass changes FAPI requirements — is there a paid relationship with the author?
Stack fit. Strong for a Laravel monolith or modular monolith already using the web middleware group, session cookies, and Eloquent users keyed by NRIC/FIN. Weakest fit where you use a stateless API-only architecture (no session), a non-Laravel service, or a multi-tenant model requiring per-tenant SingPass clients — all three are solvable but are custom work, not package work.
Migration path. Two scenarios. (a) Greenfield: adopt the package as the sole SingPass path, wire the listener to your user provisioning, done. (b) Replacing a legacy SingPass OIDC/SAML or CorpPass integration: run the package on new routes (e.g. /ndi/sp/login alongside your existing /singpass/login) behind a feature flag, dual-run in staging, then cut over per-flow rather than big-bang. CorpPass is the highest-migration-cost flow because it introduces an entity+actor hierarchy (sub = UEN, act.sub = person) that your data model likely doesn't yet express.
Compatibility considerations. Pin an exact version; do not use a floating constraint on a security-critical dependency. Confirm the package's Laravel version ceiling against your framework upgrade calendar — an auth package lagging a Laravel major blocks your whole upgrade. Verify the web middleware group is the right one for your SPA (CORS vs. same-origin), and that the /ndi/* prefix doesn't collide with existing routes or your WAF/reverse-proxy rules.
Sequencing.
nationality/dob fetch. This validates the discovery-cache path and proves the JWKS endpoint is publicly reachable.state/PKCE enforcement, token scope validation (shouldCallUserInfo failing loudly on malformed tokens is a good sign), and that NRIC is only requested when user.identity is genuinely needed.SingPassSuccessfulLoginEvent handling against your User model; migrate from the deprecated SingPassUser::getNric() to the nric property immediately. For CorpPass, model entity/actor explicitly.redirect_url; ensure the failure redirect (NDI_FAILURE_REDIRECT_URL or the login route fallback) renders a sane error state.Maintenance. The package is small and largely declarative, so day-to-day maintenance is low — but it is a security dependency, which means it needs an owner, a version pin, and a review cadence tied to SingPass/CorpPass specification changes (FAPI 2.0 profiles do evolve; the README's insistence on a pushed_authorization_request_endpoint is already a recent-generation requirement). Budget for a fork or vendor patch if upstream goes quiet — the code volume is small enough that forking is realistic.
Support. No commercial support is implied. Assume self-support, and plan for a named internal owner who understands OAuth/OIDC and DPoP. If the organisation cannot staff that, the vendor risk becomes existential for this flow. Establish a written escalation path before production, and consider a paid engagement with the author for guaranteed response.
Scaling. The flow is request/redirect-bound, not compute-bound, so throughput is not the constraint — the IdP's rate limits and your cache backend are. Enable NDI_LOGS_ENABLED selectively and with redaction (tokens, NRIC, MyInfo payloads must never land in logs). Cache discovery responses in a shared store (Redis) with sensible TTLs. The /ndi/jwks endpoint is public and cheap; put it behind your CDN and monitor it as a health signal, since IdP token validation depends on it.
Failure modes to instrument. (1) Discovery cache poisoned or stale → pushed_authorization_request_endpoint missing → all logins fail; monitor at the discovery-cache layer, not just at the login endpoint. (2) Clock skew → DPoP/JWT assertion rejection; ensure NTP across nodes. (3) Key mismatch or JWKS not yet propagated at the IdP → blanket token exchange failures after rotation; rotate with overlap. (4) state mismatch on multi-node with a non-shared session/cache driver — insidious, intermittent, hard to reproduce. (5) Scope mismatch: MyInfo requests outside available_scopes are rejected by the package; CorpPass authinfo/tpauthinfo requested without UserInfo handling yields an event you don't consume. (6) Malformed access token throwing UserInfoRequestException — deliberately loud, so alert on it rather than swallowing it. (7) NRIC absent because user.identity wasn't requested — this surfaces as a listener-level exception; make the error message actionable.
Ramp-up. Developer ramp-up is short — a day to read the README and trace one flow, since the architecture is well documented and the config surface is legible. Organisational ramp-up is longer: someone must own SingPass test-environment access, corporate key custody, the PDPA/consent story for MyInfo data retention, and the incident runbook for "nobody can log in." Treat the first integration as a 4–6 week effort with a security review gate, not a sprint ticket, and do not adopt it without an exit plan given the 0-dependents dependency profile.
How can I help you explore Laravel packages today?