accredifysg/singpass-login
Laravel package for SingPass Login, MyInfo, and CorpPass using FAPI 2.0-style auth: OpenID discovery, Pushed Authorization Requests (PAR) with DPoP, PKCE, and private-key JWT client assertions. Includes shared services and thin provider controllers.
The monolithic singpass-login.php has been split into focused config files with properly namespaced environment variables:
| Config file | Purpose | Env prefix |
|---|---|---|
config/ndi.php |
Shared NDI infrastructure (JWKS, signing, DPoP, logging, JWKS endpoint) | NDI_ |
config/singpass-login.php |
SingPass Login (credentials, routes, listener, login scopes) | SINGPASS_ |
config/myinfo.php |
MyInfo (credentials, routes, available scopes, login scopes) | MYINFO_ |
config/corppass-login.php |
CorpPass (unchanged) | CORPPASS_ |
Renamed environment variables:
| Old | New |
|---|---|
SINGPASS_SIGNING_KID |
NDI_SIGNING_KID |
SINGPASS_JWKS |
NDI_JWKS |
SINGPASS_PRIVATE_JWKS |
NDI_PRIVATE_JWKS |
SINGPASS_DPOP_SIGNING_ALGORITHM |
NDI_DPOP_SIGNING_ALGORITHM |
SINGPASS_LOGS_ENABLED |
NDI_LOGS_ENABLED |
SINGPASS_JWKS_URL |
NDI_JWKS_URL |
SINGPASS_MYINFO_CLIENT_ID |
MYINFO_CLIENT_ID |
SINGPASS_MYINFO_REDIRECT_URI |
MYINFO_REDIRECT_URI |
SINGPASS_USE_DEFAULT_MYINFO_ROUTES |
MYINFO_USE_DEFAULT_ROUTES |
SINGPASS_MYINFO_AUTHENTICATION_URL |
MYINFO_AUTHENTICATION_URL |
SINGPASS_MYINFO_CALLBACK_URL |
MYINFO_CALLBACK_URL |
Config validation: ProviderConfig factory methods now validate required config values (discovery_endpoint, client_id, redirect_uri, domain) before construction and throw MissingConfigException with a clear message identifying the missing key, instead of allowing PHP TypeError on null values.
The package now implements the full FAPI 2.0 security profile as required by SingPass, MyInfo, and CorpPass:
request_uri for the browser redirect.jti claims instead of client secrets.OpenIdConfigurationDto that enforces the presence of required endpoints.[SingPass] prefix. Logging is gated behind NDI_LOGS_ENABLED=true (defaults to false). Sensitive values (client_assertion, code_verifier, id_token, access_token) are automatically redacted. Session IDs are logged on both login initiation and callback to help diagnose session-loss issues.CorpPass FAPI 2.0 integration is now supported alongside SingPass and MyInfo:
config/corppass-login.php: CorpPass-specific configuration (client credentials, scopes, routes, listener).ProviderConfig::corpPass(): Factory method for CorpPass provider configuration.CorpPassUser model: Hierarchical entity + actor data model reflecting CorpPass ID token structure (sub for entity, act.sub for actor).CorpPassSuccessfulLoginEvent: Fired on successful CorpPass login, carries a CorpPassUser.CorpPassDataRetrievedEvent: Fired when UserInfo scopes (authinfo, tpauthinfo) return authorization data.CorpPass\LoginController and CorpPass\LoginCallbackController.GET /ndi/cp/login and GET /ndi/cp/callback (configurable, toggleable).MyInfo has been separated into its own distinct flow with dedicated routes and controllers:
GET /ndi/mi/initiate and GET /ndi/mi/callback replace the previous shared login route.MYINFO_CLIENT_ID and MYINFO_REDIRECT_URI (in config/myinfo.php) with an independent OpenID discovery cache.SingPass\MyInfoController and SingPass\MyInfoCallbackController.MyInfoDataRetrievedEvent: Fired when UserInfo data is retrieved (including empty responses).The internal architecture has been restructured around a shared FAPI 2.0 service layer with thin, provider-specific controllers:
FapiAuthenticationService: Orchestrates PAR, DPoP, PKCE, scope validation, session storage, and authorization URL construction for any provider.FapiCallbackService: Orchestrates session validation, CSRF verification, token exchange, JWE/JWS processing, and UserInfo/ID token routing for any provider.ProviderConfig DTO: Encapsulates per-provider configuration (discovery endpoint, client ID, redirect URI, cache key, scopes). Factory methods: singPassLogin(), singPassMyInfo(), corpPass().FapiCallbackResult DTO: Returned by FapiCallbackService::processCallback() with idTokenPayload and/or userInfoData.FapiSessionContext DTO: Holds validated session data (code, state, DPoP key, code verifier, client credentials).OpenIdConfigurationDto: Typed, validated DTO for OpenID discovery configuration.TokenResponseDto: DTO for token exchange responses with hasAccessToken() PHPStan assertion.state parameter is stored in the session during PAR and verified on callback. Mismatched states are rejected.LOGIN- / MYINFO- state prefixes have been removed. Credential routing is now handled securely via session storage.ProviderConfig::singPassLogin() restricts availableScopes to loginScopes, preventing non-login scopes from being requested through the login endpoint.finally blocks: All callback controllers clean up session data (DPoP keys, code verifiers, client credentials) in finally blocks, preventing stale data on exceptions.TokenExchangeService checks HTTP status, parses OAuth error responses (error / error_description), and validates the presence of id_token before returning.OpenIdConfigurationDto from cache perform instanceof checks with service-specific exceptions.Each flow can now be independently enabled or disabled:
| Environment Variable | Default | Controls |
|---|---|---|
SINGPASS_USE_DEFAULT_ROUTES |
true |
SingPass Login routes |
MYINFO_USE_DEFAULT_ROUTES |
true |
MyInfo routes |
CORPPASS_USE_DEFAULT_ROUTES |
true |
CorpPass routes |
The JWKS endpoint (/ndi/jwks) is always registered as it is shared across all providers.
| Service | Purpose |
|---|---|
DPoPService |
Ephemeral key generation, DPoP proof JWT creation, key storage/retrieval |
PushedAuthorizationRequestService |
PAR endpoint requests with DPoP |
FapiAuthenticationService |
Auth initiation orchestrator (discovery, PAR, DPoP, PKCE, session) |
FapiCallbackService |
Callback orchestrator (session validation, token exchange, routing) |
ScopeValidationService |
Scope parsing and validation against provider config |
| Exception | Purpose |
|---|---|
MissingConfigException |
Required config value is null — thrown by ProviderConfig factories with a message identifying the missing key |
AuthenticationErrorException |
OAuth error returned to callback (error / error_description query params) |
PushedAuthorizationRequestException |
PAR endpoint errors (includes OAuth error codes) |
| Removed | Replacement |
|---|---|
SingPassLogin (main service class) |
FapiAuthenticationService + FapiCallbackService |
SingPassLoginFacade |
Removed entirely; use dependency injection |
SingPassLoginInterface |
Removed entirely; use service interfaces |
GetAuthenticationEndpointController |
SingPass\LoginController + SingPass\MyInfoController |
GetSingPassCallbackController |
SingPass\LoginCallbackController + SingPass\MyInfoCallbackController |
GetSingPassTokenService |
TokenExchangeService |
GetSingPassJwksService |
JwksService |
| v2 Name | v3 Name |
|---|---|
SingPassGetEndpointException |
AuthFlowException |
SingPassAuthenticationErrorException |
AuthenticationErrorException |
SingPassTokenException |
TokenExchangeException |
SingPassJwksException |
JwksException |
SingPassJwtService |
JwtService |
GetSingPassTokenServiceInterface |
TokenExchangeServiceInterface |
GetSingPassJwksServiceInterface |
JwksServiceInterface |
SingPassJwtServiceInterface |
JwtServiceInterface |
{ "redirect_url": "..." } JSON instead of performing a server-side redirect. Clients must fetch the endpoint (with same-origin credentials) and navigate to the returned URL.web group) for DPoP key storage, PKCE verifiers, and CSRF state verification.SingPassUser model changes: The sub claim is now a UUID (not a composite NRIC/UUID string). NRIC/FIN is available on the readonly nric property (from sub_attributes.identity_number) when the user.identity scope is requested (?string). The accessor getNric() is deprecated in favour of $nric./ndi/mi/initiate and /ndi/mi/callback instead of sharing the SingPass login route. MyInfo has its own config file (config/myinfo.php) with separate client credentials (MYINFO_CLIENT_ID / MYINFO_REDIRECT_URI).pushed_authorization_request_endpoint. Incomplete discovery responses throw OpenIdDiscoveryException.singpass-login.php into four files (ndi.php, singpass-login.php, myinfo.php, corppass-login.php). Several environment variables have been renamed — see the "Config Split & Validation" section above. Re-publish config after upgrading.php artisan vendor:publish --provider="Accredifysg\SingPassLogin\SingPassLoginServiceProvider" --tag="config" --forceSINGPASS_SIGNING_KID → NDI_SIGNING_KID, SINGPASS_MYINFO_CLIENT_ID → MYINFO_CLIENT_ID). See README for the full list.fetch + window.location.assign(redirect_url).SingPassLogin, SingPassLoginInterface, or the facade, switch to FapiAuthenticationService / FapiCallbackService via dependency injection.$event->getSingPassUser()->nric for NRIC/FIN (add a null check — requires the user.identity scope). Avoid getNric(); it is deprecated.login_scopes in singpass-login.php and available_scopes in myinfo.php to match your application's needs.MyInfoDataRetrievedEvent, UserInfo endpoint support (JWE decryption + JWS verification), and updated callback handling for the MyInfo path.phpstan/phpstan from 2.1.32 to 2.1.33.GetUserInfoService with JWE/JWS processing, ScopeValidationService for validating requested scopes, MyInfoDataRetrievedEvent, and UserInfo-specific exceptions (UserInfoRequestException, UserInfoDecryptionException, UserInfoVerificationException).TokenResponseDto: New DTO for token exchange responses carrying both idToken and optional accessToken.GetAuthenticationEndpointController to determine login vs MyInfo flows.login_scopes and available_scopes in singpass-login.php.SINGPASS_MYINFO_CLIENT_ID, SINGPASS_MYINFO_REDIRECT_URI).phpstan.neon) for static analysis.SingPassJwtService with additional JWT processing methods for UserInfo tokens.GetSingPassTokenService to return TokenResponseDto instead of raw ID token string.GetAuthenticationEndpointController to handle scope-based MyInfo/Login routing.[@param](https://github.com/param) annotations.orchestra/testbench from 10.4.0 to 10.8.0.web-token/jwt-framework from 4.0.4 to 4.1.2.guzzlehttp/guzzle from 7.9.3 to 7.10.0.laravel/pint from 1.25.1 to 1.26.0.phpstan/phpstan from 2.1.17 to 2.1.32.GetSingPassTokenServiceInterface::getToken() now returns TokenResponseDto instead of string.login_scopes, available_scopes, myinfo_client_id, myinfo_redirect_uri.SingPassGetEndpointException: New exception thrown when the authentication endpoint is accessed directly or with missing parameters.orchestra/testbench from 9.13.1 to 10.4.0.phpstan/phpstan from 2.1.13 to 2.1.17.laravel/pint from 1.22.0 to 1.22.1.symfony/clock from 7.2.0 to 7.3.0.No functional changes from v1.1.0. Tag re-release.
CodeChallengeVerifierService implementing S256 code challenge for the authorization flow.laravel/pint from 1.18.1 to 1.22.0.phpstan/phpstan from 2.1.6 to 2.1.13.guzzlehttp/guzzle from 7.9.2 to 7.9.3.web-token/jwt-framework from 4.0.3 to 4.0.4.SingPassLoginException to redirect with error details instead of a generic failure response.Initial release.
SingPassUser model with NRIC and UUID extraction from ID token sub claim.SingPassSuccessfulLoginEvent fired on successful authentication.SingPassSuccessfulLoginListener).singpass-login.php config and environment variables.How can I help you explore Laravel packages today?