accredifysg / singpass-login
A Laravel package for integrating SingPass Login and MyInfo
Installs: 1 970
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 2
Open Issues: 3
Type:laravel-package
pkg:composer/accredifysg/singpass-login
Requires
- php: ^8.2
- ext-gmp: *
- ext-json: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.0
- illuminate/contracts: ^10.0||^11.0||^12.0
- spomky-labs/aes-key-wrap: ^7.0
- symfony/clock: ^7.0
- web-token/jwt-framework: ^4.0.1
Requires (Dev)
- fakerphp/faker: ^1.23
- laravel/pint: ^1.16
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-mockery: ^2.0
- phpunit/phpunit: ^11.2
This package is auto-updated.
Last update: 2025-12-15 01:12:52 UTC
README
PHP Laravel Package for SingPass Login and MyInfo
Installation
You can install the package via composer:
composer require accredifysg/singpass-login
Add the following variables to your .env file.
# SingPass variables
SINGPASS_CLIENT_ID=
SINGPASS_REDIRECT_URI=
SINGPASS_DOMAIN=
SINGPASS_DISCOVERY_ENDPOINT=
SINGPASS_SIGNING_KID=
SINGPASS_JWKS=
SINGPASS_PRIVATE_JWKS=
# Default Routes
SINGPASS_USE_DEFAULT_ROUTES=true
SINGPASS_JWKS_URL=/sp/jwks
SINGPASS_AUTHENTICATION_URL=/sp/login
SINGPASS_CALLBACK_URL=/sp/callback
# Default Listener
SINGPASS_USE_DEFAULT_LISTENER=true
# Optional MyInfo envs if you want to use MyInfo integration
SINGPASS_MYINFO_CLIENT_ID=
SINGPASS_MYINFO_REDIRECT_URI=
Publish the config file
php artisan vendor:publish --provider="Accredifysg\SingPassLogin\SingPassLoginServiceProvider" --tag="config"
Optionally, you can publish the listener that will listen to the SingPassLoginEvent and log the user in
php artisan vendor:publish --provider="Accredifysg\SingPassLogin\SingPassLoginServiceProvider" --tag="listener"
Usage and Customisations
Controllers and Routes
There are three default controllers that handle the login process
GetJwksEndpointController exposes your application's JWKS endpoint to be registered with SingPass.
The default route for this controller is /sp/jwks
GetAuthenticationEndpointController provides the authentication endpoint to redirect the client's browser to.
The default route for this controller is /sp/login
PostSingPassCallbackController handles the callback from SingPass, and kick-starts the login process.
The default route for this controller is /sp/callback
If you prefer to set your own routes you can set SINGPASS_USE_DEFAULT_ROUTES to false,
then edit SINGPASS_JWKS_URL, SINGPASS_CALLBACK_URL, and SINGPASS_AUTHENTICATION_URL in
your .env file and map your own routes.
If you prefer to write your own controllers you can define them in the config file
singpass-login.php as get_jwks_endpoint_controller, post_singpass_callback_controller and get_authentication_endpoint_controller
Listener
If you published the default listener, you should edit it and map your user retrieval via NRIC accordingly.
public function handle(SingPassSuccessfulLoginEvent $event): RedirectResponse { $singPassUser = $event->getSingPassUser(); $nric = $singPassUser->getNric(); $user = User::where('nric', '=', $nric)->first(); // Map to your own model that stores the users' NRIC or UUID if (! $user) { throw new SingPassLoginException; } Auth::login($user); }
If you prefer to write your own, you can set SINGPASS_USE_DEFAULT_LISTENER to false in
your .env and replace listener_class in the config file singpass-login.php
MyInfo Integration
This package supports retrieving user data from MyInfo through scope-based data retrieval. By default, the package performs authentication-only flow using the openid scope. To retrieve additional user data, you can request specific MyInfo scopes during the authentication process.
How It Works
MyInfo functionality is scope-driven:
- Authentication Only: When only the
openidscope is requested (default), the package performs standard authentication without calling the UserInfo endpoint - MyInfo Data Retrieval: When additional MyInfo scopes are requested, the package calls the UserInfo endpoint after successful authentication to retrieve the consented user data
Requesting MyInfo Scopes
Pass scopes as query parameters when redirecting users to the authentication endpoint:
From JavaScript/Frontend:
// Basic authentication only (default behavior) window.location.href = '/sp/login'; // Request basic profile information const scopes = ['openid', 'name', 'email', 'mobileno']; window.location.href = `/sp/login?scopes=${scopes.join(',')}`; // Request extended user data const extendedScopes = [ 'openid', 'name', 'email', 'mobileno', 'nationality', 'dob' ]; window.location.href = `/sp/login?scopes=${extendedScopes.join(',')}`;
From Laravel Controller:
public function redirectToSingPass() { $scopes = ['openid', 'name', 'email', 'mobileno']; return redirect('/sp/login?' . http_build_query(['scopes' => implode(',', $scopes)])); }
Available MyInfo Scopes
For the complete and up-to-date list of available MyInfo data items and their descriptions, refer to the official MyInfo Data Catalog:
MyInfo Data Catalog Documentation
The package validates requested scopes against the available_scopes configuration. Invalid scopes are filtered out and logged as warnings.
Handling MyInfo Data
When MyInfo scopes are requested and data is successfully retrieved, the package emits a MyInfoDataRetrievedEvent instead of the standard SingPassSuccessfulLoginEvent.
MyInfoDataRetrievedEvent
Create a listener to handle the MyInfo data:
<?php namespace App\Listeners; use Accredifysg\SingPassLogin\Events\MyInfoDataRetrievedEvent; class MyInfoDataRetrievedListener { public function handle(MyInfoDataRetrievedEvent $event): void { $myInfoData = $event->getMyInfoData(); $state = $event->getState(); // Update user profile with MyInfo data if ($myInfoData) { $user->update([ 'name' => $myInfoData['name']['value'] ?? null, 'email' => $myInfoData['email']['value'] ?? null, 'mobile' => $myInfoData['mobileno']['value'] ?? null, 'nationality' => $myInfoData['nationality']['value'] ?? null, 'date_of_birth' => $myInfoData['dob']['value'] ?? null, ]); } } }
MyInfo Data Structure
MyInfo data is returned as an associative array. Each field typically contains:
value: The actual data valuesource: Data source identifier (e.g., '1' for government-verified)classification: Data classification levellastupdated: Timestamp of last update
Example structure:
[
'sub' => 's=S1234567A,u=UUID',
'name' => [
'value' => 'John Doe',
'source' => '1',
'classification' => 'C',
'lastupdated' => '2023-01-15'
],
'email' => [
'value' => 'john@example.com',
'source' => '2',
'classification' => 'C',
'lastupdated' => '2023-01-15'
],
'mobileno' => [
'value' => '+6591234567',
'source' => '2',
'classification' => 'C',
'lastupdated' => '2023-01-15'
],
// Additional fields based on requested scopes
]
Registering the Listener
Register the listener in your EventServiceProvider:
<?php namespace App\Providers; use Accredifysg\SingPassLogin\Events\MyInfoDataRetrievedEvent; use Accredifysg\SingPassLogin\Events\SingPassSuccessfulLoginEvent; use App\Listeners\MyInfoDataRetrievedListener; use App\Listeners\SingPassSuccessfulLoginListener; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { protected $listen = [ // Authentication only (no MyInfo scopes) SingPassSuccessfulLoginEvent::class => [ SingPassSuccessfulLoginListener::class, ], // MyInfo data retrieval (with additional scopes) MyInfoDataRetrievedEvent::class => [ MyInfoDataRetrievedListener::class, ], ]; }
Event Flow
- Authentication Only: When only
openidscope is requested →SingPassSuccessfulLoginEventis emitted - MyInfo Data Retrieval: When additional MyInfo scopes are requested →
MyInfoDataRetrievedEventis emitted
This separation allows you to handle authentication-only flows differently from flows that include MyInfo data retrieval.
Backward Compatibility
Existing implementations continue to work without any changes:
- Default behavior remains authentication-only with
openidscope SingPassSuccessfulLoginEventis still emitted for authentication-only flows- No configuration changes required for existing applications
Exceptions
<?php use Accredifysg\SingPassLogin\Exceptions\JweDecryptionFailedException; use Accredifysg\SingPassLogin\Exceptions\JwksInvalidException; use Accredifysg\SingPassLogin\Exceptions\JwtDecodeFailedException; use Accredifysg\SingPassLogin\Exceptions\JwtPayloadException; use Accredifysg\SingPassLogin\Exceptions\OpenIdDiscoveryException; use Accredifysg\SingPassLogin\Exceptions\SingPassJwksException; use Accredifysg\SingPassLogin\Exceptions\SingPassTokenException; use Accredifysg\SingPassLogin\Exceptions\SingPassLoginException; // MyInfo-specific exceptions use Accredifysg\SingPassLogin\Exceptions\UserInfoRequestException; use Accredifysg\SingPassLogin\Exceptions\UserInfoDecryptionException; use Accredifysg\SingPassLogin\Exceptions\UserInfoVerificationException;
MyInfo Exception Handling
When retrieving MyInfo data, the following exceptions may be thrown:
UserInfoRequestException: Thrown when the UserInfo endpoint HTTP request fails. Includes HTTP status code and endpoint URL.UserInfoDecryptionException: Thrown when the UserInfo JWE token decryption fails. Includes decryption failure details.UserInfoVerificationException: Thrown when the UserInfo JWS token verification fails. Includes verification failure reason.
All MyInfo exceptions extend SingPassLoginException and can be caught and handled in your application:
use Accredifysg\SingPassLogin\Exceptions\UserInfoRequestException; use Accredifysg\SingPassLogin\Exceptions\UserInfoDecryptionException; use Accredifysg\SingPassLogin\Exceptions\UserInfoVerificationException; try { // MyInfo data retrieval happens automatically during callback } catch (UserInfoRequestException $e) { // Handle UserInfo endpoint failure Log::error('MyInfo request failed: ' . $e->getMessage()); } catch (UserInfoDecryptionException $e) { // Handle decryption failure Log::error('MyInfo decryption failed: ' . $e->getMessage()); } catch (UserInfoVerificationException $e) { // Handle verification failure Log::error('MyInfo verification failed: ' . $e->getMessage()); }