Skip to content

Add support for FAPI for Singpass Login#800

Merged
LoneRifle merged 13 commits intoopengovsg:mainfrom
javierpng:feat/fapi2
Mar 9, 2026
Merged

Add support for FAPI for Singpass Login#800
LoneRifle merged 13 commits intoopengovsg:mainfrom
javierpng:feat/fapi2

Conversation

@javierpng
Copy link
Copy Markdown
Contributor

@javierpng javierpng commented Mar 6, 2026

Problem

Add support for FAPI flow

Closes #799

Solution

The existing v2-OIDC flow for Singpass Login is deprecated and new RPs are advised to use the FAPI flow for integration with Singpass Login.

Features:

  • Created a new /v3/fapi/par endpoint to create a sign request.
  • /v3/fapi/auth will no longer take in the parameters that are currently present in the OIDC flow.
  • v3/fapi/token will require a DPoP token

Improved Security

  • A Pushed Authorization Request (PAR) is used to initialize a login session. Prevents Authorization request tampering via the GET query
  • Use of proof-of-possession tokens (DPoP)
  • PKCE

Configuration
process.env.FAPI_CLIENT_JWKS_ENDPOINT - for configuring the JWKS endpoint

Allowed Scopes
Currently, only the following are supported

  • openid
  • uinfin
  • user.identity

Tests

As tests are not a norm in this repository, I tested manually with my test application.
A test endpoint is created for simulating the FAPI flow without a server setup.

Limitations

  • The current PR only covers the Login flow, scopes are only limited to openid and user.identity
  • Client_id, redirect_uri, can accept any values.
  • Ephemeral keys, state and nonce are not checked for duplication.
  • Since there is no Redis, I am relying on ExpiryMap for session management and using client_id as key instead of redirect_uri. If you are planning to run multiple login sessions at once, suggest to use client_id:<state> as the client_id

Copy link
Copy Markdown
Collaborator

@LoneRifle LoneRifle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm otherwise

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - there should be naming consistency - if you have a fapi.service.js, you should have a fapi.controller.js too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved, thanks!

Comment thread lib/express/fapi/utils.js Outdated
const { readFileSync } = require('fs')
const path = require('path')

const issuer = 'http://localhost:5156/v3/fapi'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MockPass auth endpoints would typically construct the issuer in a manner that trusts the requests, to ensure that developers can reconfigure mockpass as they wish without worrying

You may wish to look at the other endpoint implementations to see how this might be done.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved as well

Comment thread lib/express/fapi/utils.js Outdated
}

const FAPI_PATH = '/v3/fapi'
let issuer = `http://localhost:5156${FAPI_PATH}` //default issuer
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the prior review. Please refactor as needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default value if running locally.

I added a getFapiOpenIdConfiguration(req) that will be called by the service layer. This will determine the protocol and host and update the FapiConfiguration using the values in req during runtime.

e.g.

app.get(
    `${FapiUtils.FAPI_PATH}/.well-known/openid-configuration`,
    (req, res) => {
      return res.send(FapiUtils.getFapiOpenIdConfiguration(req))
    },
  )

When called, it will ensure that the endpoints are tagged to the developer's host

function getIssuerFromRequest(req) {
  return `${req.protocol}://${req.get('host')}${FAPI_PATH}`
}

function getFapiOpenIdConfiguration(req) {
  const issuer = getIssuerFromRequest(req)
  return {
    ...fapiOidcConfiguration,
    issuer,
    jwks_uri: `${issuer}/.well-known/keys`,
    pushed_authorization_request_endpoint: `${issuer}/par`,
    authorization_endpoint: `${issuer}/auth`,
    token_endpoint: `${issuer}/token`,
  }
} 

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it then be better to not actually have the default value at all, given that this would change at runtime? Having the hard-coded value and then discarding it at runtime strikes me as a code smell, and may catch out people who are relatively new to the codebase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I have removed the issuer from the code and refactored it. Thanks!

Copy link
Copy Markdown
Collaborator

@LoneRifle LoneRifle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@LoneRifle LoneRifle merged commit 034bc05 into opengovsg:main Mar 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Singpass Login using FAPI2 flow

2 participants