shokanshi / singpass-myinfo
Laravel Socialite Provider For Singpass MyInfo v5
Fund package maintenance!
shokanshi
Installs: 6
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/shokanshi/singpass-myinfo
Requires
- php: ^8.3
- illuminate/contracts: ^11.0||^12.0
- laravel/socialite: ^5.23
- spatie/laravel-package-tools: ^1.16
- spomky-labs/aes-key-wrap: ^7.0
- web-token/jwt-framework: ^4.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.25
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
README
The purpose of this Laravel package is to make it very easy for PHP (8.3+) developers to integrate Singpass MyInfo v5.
FAPI 2.0 support is currently not available as Singpass staging and production servers will only be ready in December 2025 and January 2026 respectively.
Requirements
- PHP ≥ 8.3
- Laravel ≥ 11.0
Support Me
A sponsor will be greatly appreciated but not required to use this package. 😊
Installation
You can install the package via composer:
composer require shokanshi/singpass-myinfo
Setting Up Private Keys
The package will attempt to load the private keys from storage/app.
❌ DO NOT store the private keys in ./storage/app/public folder! They will be publicly accessible!
If you have not already done so, create a secure folder within storage/app in your project folder.
Create private key for signing:
openssl ecparam -name prime256v1 -genkey -noout -out ./storage/app/secure/your-singpass-signing-private.pem
Create private key for decryption:
openssl ecparam -name prime256v1 -genkey -noout -out ./storage/app/secure/your-singpass-decryption-private.pem
Add the following variables to your .env file and adjust accordingly to your app. The following is just an example.
# Singpass variables SINGPASS_CLIENT_ID= # Base folder is ./storage/app SINGPASS_SIGNING_PRIVATE_KEY_FILE=secure/your-singpass-signing-private.pem SINGPASS_SIGNING_PRIVATE_KEY_PASSPHRASE= # Base folder is ./storage/app SINGPASS_DECRYPTION_PRIVATE_KEY_FILE=secure/your-singpass-decryption-private.pem SINGPASS_DECRYPTION_PRIVATE_KEY_PASSPHRASE= SINGPASS_OPENID_DISCOVERY_ENDPOINT=https://stg-id.singpass.gov.sg/.well-known/openid-configuration # for Singpass login, set openid as the only scope. Additional scopes (space separated within double quotes) will switch to MyInfo flow SINGPASS_SCOPES="openid" # Default routes SINGPASS_AUTHORIZATION_ENDPOINT=sp/login SINGPASS_CALLBACK_ENDPOINT=sp/callback SINGPASS_JWKS_ENDPOINT=sp/jwks
Checking If It Work Right Out Of The Box For You
Remember to create your Singpass application at Singpass Developer Portal before you proceed to test.
Assuming you are using the default setup and filled up the values in .env file:
- Test your jwks endpoint to see if Singpass is able to access it:
https://your-company.com/sp/jwks
- Test if it redirects to Singpass auth endpoint:
https://your-company.com/sp/login
Configuration
You can publish the config file with:
php artisan vendor:publish --tag="singpass-myinfo-config"
This is the content of the published config file:
return [ // default to Singpass staging 'openid_discovery_endpoint' => env('SINGPASS_OPENID_DISCOVERY_ENDPOINT', 'https://stg-id.singpass.gov.sg/.well-known/openid-configuration'), 'client_id' => env('SINGPASS_CLIENT_ID'), // this setting is here because socialite requires it to be defined. SingpassProvider will always overwrite it to route('singpass.callback') 'redirect' => env('SINGPASS_REDIRECT_URI'), // the private key file that your application will be used for signing 'signing_private_key_passphrase' => env('SINGPASS_SIGNING_PRIVATE_KEY_PASSPHRASE', ''), 'signing_private_key_file' => env('SINGPASS_SIGNING_PRIVATE_KEY_FILE'), // the private key file that your application will be used for decryption 'decryption_private_key_passphrase' => env('SINGPASS_DECRYPTION_PRIVATE_KEY_PASSPHRASE', ''), 'decryption_private_key_file' => env('SINGPASS_DECRYPTION_PRIVATE_KEY_FILE'), // used by socialite. leave it empty since Singpass uses client assertion 'client_secret' => '', // default to Singpass login if SINGPASS_SCOPES is blank. for MyInfo, define additional scopes that are space separated // e.g. "openid uinfin name sex race dob birthcountry passportnumber" 'scopes' => env('SINGPASS_SCOPES', 'openid'), // this is the route that will be used to redirect to Singpass login page // you can customize this in .env file 'authorization_endpoint' => env('SINGPASS_AUTHORIZATION_ENDPOINT', 'sp/login'), // the controller that will handle the redirection to Singpass login page // to customize, you can replace it with your own controller in this config file 'authorization_endpoint_controller' => GetAuthorizationController::class, // this is the route that will be called when Singpass redirects back after authentication // you can customize this in .env file 'callback_endpoint' => env('SINGPASS_CALLBACK_ENDPOINT', 'sp/callback'), // the controller that will handle the callback from Singpass after login // to customize, you can replace it with your own controller in this config file 'callback_endpoint_controller' => GetCallbackController::class, // this is the url that Singpass will call to retrieve your public jwks for signing and encryption // you can customize this in .env file 'jwks_endpoint' => env('SINGPASS_JWKS_ENDPOINT', 'sp/jwks'), // the controller that Singpass portal will use to retrieve your application jwks // typically you won't want to change it unless you want to implement key rotation logic 'jwks_endpoint_controller' => GetJwksController::class, ];
Routes
There are three default routes that you can customize, namely:
SINGPASS_AUTHORIZATION_ENDPOINT=sp/login SINGPASS_CALLBACK_ENDPOINT=sp/callback SINGPASS_JWKS_ENDPOINT=sp/jwks
You can access the routes via name in your Laravel codes:
route('singpass.login'); route('singpass.callback'); route('singpass.jwks');
Custom Routes
If you prefer the authentication url to be https://your-company.com/sp/auth, you can update SINGPASS_AUTHORIZATION_URL to sp/auth:
SINGPASS_AUTHORIZATION_URL=sp/auth
Custom Controllers
You can customize the default controller via the singpass-myinfo.php config file.
'authorization_endpoint_controller' => GetAuthorizationController::class, 'callback_endpoint_controller' => GetCallbackController::class, 'jwks_endpoint_controller' => GetJwksController::class,
Example:
To create an authentication controller that will switch between local and production environment
php artisan make:controller MySingpassAuthController
In singpass-myinfo.php config file:
'authorization_endpoint_controller' => MySingpassAuthController::class,
In MySingpassAuthController.php:
class MySingpassAuthController extends Controller { public function __invoke(Request $request) { return singpass() ->when(app()->environment('local'), function($singpass) { $singpass ->setClientId('staging client id') ->setOpenIdDiscoveryUrl('https://stg-id.singpass.gov.sg/.well-known/openid-configuration') ->addSigningKey(Storage::disk('local')->get('stage_signing_key_1.pem')) ->addDecryptionKey(Storage::disk('local')->get('stage_decryption_key_1.pem')); }) ->when(app()->environment('production'), function($singpass) { $singpass ->setClientId('production client id') ->setOpenIdDiscoveryUrl('https://id.singpass.gov.sg/.well-known/openid-configuration') ->addSigningKey(Storage::disk('local')->get('prod_signing_key_1.pem')) ->addDecryptionKey(Storage::disk('local')->get('prod_decryption_key_1.pem')); }) ->redirect(); } }
ℹ️ Note:
- For the above example, the same customization has to be applied to
callback_endpoint_controllerandjwks_endpoint_controllersince the endpoint is now based on environment of the application. - The above is just an example to illustrate how you may customize the controllers.
Using the Socialite Provider
singpass(): SingpassProvider
A helper method that return the SingpassProvider Socialite object.
In the event where singpass() is not available (likely in conflict with another helper method in your project), you can still access the Socialite by calling Socialite::driver('singpass').
user(): \Laravel\Socialite\Contracts\User
Return the Socialite user object.
Methods Available
If you have a multitenancy application and would like to allow onboarding of individual tenant onto Singpass, the following methods will be useful to you. You can setup custom controllers (like the example above) to handle the aspect of multitenancy with them.
redirect(): \Illuminate\Http\RedirectResponse
Redirect the user of the application to the provider's authentication screen.
To retrieve the redirect url, you can call singpass()->redirect()->getTargetUrl().
setClientId(string $clientId): self
Overwrite the value of SINGPASS_CLIENT_ID defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$clientId |
string |
Singpass client id | required |
setOpenIdDiscoveryUrl(string $url): self
Overwrite the value of SINGPASS_DISCOVERY_ENDPOINT defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$url |
string |
Singpass openid discovery endpoint | required |
setRedirectUrl(string $redirectUrl): self
Overwrite the value of SINGPASS_REDIRECT_URI defined in the .env file when called. Useful when your application have different redirects based on certain business logic.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$redirectUrl |
string |
Singpass callback endpoint | required |
addSigningKey(string $keyContent, ?string $passphrase): self
Add a new private key to the collection and overwrite the value of SINGPASS_SIGNING_PRIVATE_KEY_FILE defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$keyContent |
string |
The content of the private key pem file that will be used for signing | required |
$passphrase |
string |
The passphrase for the pem file if it is encrypted |
addSigningKeyFromJsonObject(string $json): self
Add a new private key to the collection and overwrite the value of SINGPASS_SIGNING_PRIVATE_KEY_FILE defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$json |
string |
Json encoded string of the private JWK that will be used for signing | required |
Sample json object from Singpass Demo for signing:
{
"alg": "ES256",
"kty": "EC",
"x": "tqG7PiAPD0xTBKdxDd4t8xAjJleP3Szw1CZiBjogmoc",
"y": "256TjvubWV-x-C8lptl7eSbMa7pQUXH9LY1AIHUGINk",
"crv": "P-256",
"d": "PgL1UKVpvg_GeKdxV-oUEPIDhGBP2YYZLGiZ5HXDZDI",
"use": "sig",
"kid": "my-sig-key"
}
addDecryptionKey(string $keyContent, ?string $passphrase): self
Add a new private key to the collection and overwrite the value of SINGPASS_DECRYPTION_PRIVATE_KEY_FILE defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$keyContent |
string |
The content of the private key pem file that will be used for decryption | required |
$passphrase |
string |
The passphrase for the pem file if it is encrypted |
addDecryptionKeyFromJsonObject(string $json): self
Add a new private key to the collection and overwrite the value of SINGPASS_DECRYPTION_PRIVATE_KEY_FILE defined in the .env file when called.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$json |
string |
Json encoded string of the private JWK that will be used for decryption | required |
Sample json object from Singpass Demo for decryption:
{
"alg": "ECDH-ES+A256KW",
"kty": "EC",
"x": "_TSrfW3arG1Ebc8pCyT-r5lAFvCh_rJvC5HD5-y8yvs",
"y": "Sr2vpuU6gzdUiXddGnRJIroXCfdameaR1mgU49H5h9A",
"crv": "P-256",
"d": "AEabUwi3VjOOfiyoOtSGrqpl8cfhcUhNtj-xh1l-UYE",
"use": "enc",
"kid": "my-enc-key"
}
generateJwksForSingpassPortal(): array
Return an array of public keys that will be json encoded and consumed by Singpass.
when($value, ?callable $callback = null, ?callable $default = null): self
The when() method allows you to conditionally execute a closure (a function) if a given condition evaluates to true. Its primary purpose is to apply modifications to an object within a method chain, based on a dynamic condition, without having to break the chain into a traditional if statement.
In short: It's an if statement that you can use inside a method chain. See the Example above.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
$value |
boolean |
Apply the callback if the given "value" is (or resolves to) truthy | required |
$callback |
callable |
The callback when $value resolved to true | |
$default |
callable |
The callback when $value resolved to false and this parameter is defined |
Advanced Usage Example (Multitenancy + Key Rotation)
Singpass recommends key rotation on a yearly basis.
The following is an example to illustrate a more advance use case for this package that handles multitenancy and key rotation.
Spatie multitenancy package will be used for illustration.
Custom fields added to Tenant table
| Name | Type | Description | Default |
|---|---|---|---|
singpass_client_id |
varchar(255) |
Singpass client id | required |
singpass_openid_discovery_endpoint |
varchar(255) |
Singpass openid discovery endpoint id | required |
singpass_scopes |
text |
Space separated Singpass scopes | openid |
New Table: tenant_private_keys
| Name | Type | Description | Default |
|---|---|---|---|
id |
bigint |
Primary key | required |
tenant_id |
bigint |
Foreign key | required |
provider |
varchar(50) |
e.g. singpass | required |
type |
varchar(50) |
e.g. signing or decryption | required |
key_content |
text |
Encrypted pem file content | required |
passphrase |
varchar(255) |
Encrypted passphrase for signing key | required |
valid_from |
datetime |
The date the key is valid from | required |
valid_to |
datetime |
The date the key is valid to | required |
class MySingpassJwksEndpointController extends Controller { public function __invoke(Request $request) { $tenant = Tenant::current(); singpass() ->setClientId($tenant->singpass_client_id) ->setOpenIdDiscoveryUrl($tenant->singpass_openid_discovery_endpoint) ->setScopes([$tenant->singpass_scopes]); foreach ($tenant->singpassPrivateKeys() as $key) { singpass() ->when(Carbon::now()->between($key->valid_from, $key->valid_to), function($singpass) use ($key) { $singpass ->when($key->type === 'signing', function($singpass) use ($key) { $singpass->addSigningKey($key->key_content, $key->passphrase); }) ->when($key->type === 'decryption', function($singpass) use ($key) { $singpass->addDecryptionKey($key->key_content, $key->passphrase); }); }); } return response()->json(json_encode(singpass()->generateJwksForSingpassPortal())); } }
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
The code of this package is heavily influenced by the code shown in Laravel Socialite - Singpass.
You will also find some code reference from Accredifysg/SingPass-Login in this package.
License
The MIT License (MIT). Please see License File for more information.