Skip to content

Add support for new adaptor Fayda ID#1721

Open
IZO-Ong wants to merge 13 commits into
OpenFn:mainfrom
IZO-Ong:fayda-adaptor
Open

Add support for new adaptor Fayda ID#1721
IZO-Ong wants to merge 13 commits into
OpenFn:mainfrom
IZO-Ong:fayda-adaptor

Conversation

@IZO-Ong

@IZO-Ong IZO-Ong commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Add a new adaptor, @openfn/language-fayda, for Ethiopia's Fayda national digital ID system, handling authentication via eSignet's OIDC flow (JWK-signed client assertion + PKCE) and returning verified identity claims.

Addresses Issue #1141

Details

What this adds

Three operations covering the full authorize -> token -> userinfo sequence:

  • getAuthorizationUrl(options) — builds the eSignet authorization URL (no network call; redirecting the user and collecting consent happens outside OpenFn). Also generates and returns a PKCE codeVerifier on state.data.
  • getToken(code, options) — exchanges authorization code for tokens. Authenticates as the client with a private_key_jwt client assertion (signed JWT) rather than a shared secret, per eSignet's requirements. Saves access_token to state.configuration so it's available to the next step without being passed explicitly.
  • getUserInfo(accessToken, options) — fetches and decodes verified identity claims from the userinfo endpoint. Handles both a signed-JWT response and a plain-JSON response, since eSignet can return either depending on client configuration.

Key decisions

  • Scope: investigated unofficial community SDK fayda-auth-sdk, and found that it only implements a plain PKCE public-client flow and has no support for JWK/private_key_jwt client assertions, which is the auth method eSignet's sandbox actually requires.
  • Signing approach: modeled directly from the official reference implementation (National-ID-Program-Ethiopia/oidc-test-app). Uses the same library (jose, pinned to the same major version, ^5.3.0), the same claim shape (iss/sub/aud, no jti), and the same 2-hour expiry. The official reference app sends both a signed client assertion and a PKCE code_challenge/code_verifier. Added PKCE support to match, auto-generating a fresh verifier per call.

Credentials

Sandbox clientId and signing key are pending application via the developer portal (~2 business days turnaround per the issue). Endpoint URLs in the configuration schema examples are illustrative placeholders.

AI Usage

  • I have used Claude Code
  • I have used another model
  • I have not used AI

You can read more details in our
Responsible AI Policy

Review Checklist

Before merging, the reviewer should check the following items:

  • Does the PR do what it claims to do?
  • If this is a new adaptor, added the adaptor on marketing website? — I am not sure about this.
  • If this PR includes breaking changes, do we need to update any jobs in
    production? Is it safe to release? — N/A, new adaptor, nothing in production depends on it.
  • Are there any unit tests? — 9 MockAgent unit tests implemented, all passing.
  • Is there a changeset associated with this PR? Should there be? — No changeset, not needed for an unpublished package's first release.
  • If there is a changeset, was pnpm run version used to bump versions (not
    pnpm changeset version directly)? — N/A, no changeset.
  • Have you ticked a box under AI Usage?

@brandonjackson

brandonjackson commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Thanks @IZO-Ong! @josephjclark @AishaHassen @PiusKariuki this is the first adaptor contribution from the YouthTech Singapore interns helping us out this summer 🙌 Would be great if y'all could find time to take a look.

Some questions that came to mind from me while reviewing this:

  • @AishaHassen @PiusKariuki what's the best way to validate that this works?
  • all: thoughts on whether it's better to have a fayda or a generic esignet adaptor? If generic, what should it's relationship be to the Fayda adaptor? should we subclass like we have with fhir adaptors (eg fhir-eswatini)?

@AishaHassen

Copy link
Copy Markdown

Looping in @redietr here as she has much more context here. @redietr can you help answer Brandon's questions above? Also replaced myself with @redietr as the PR reviewer.

@AishaHassen
AishaHassen requested review from redietr and removed request for AishaHassen July 14, 2026 07:40
@josephjclark

josephjclark commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

The underlying issue for this isn't very well specced. @redietr @PiusKariuki can someone help me understand how Fayda would be used in a workflow, and what sort of funcionality the adaptor needs?

Like is Fayda bascically just getting an access token/JWT which is then by other jobs? Or are we using it as a gateway to acutally pull information?

@redietr

redietr commented Jul 14, 2026

Copy link
Copy Markdown

@josephjclark from my understanding is that the adaptor is intended to use Fayda as an identity service, not just as a token provider. The access token is just part of the OAuth/OIDC flow. The actual purpose is to use that token to verify a person's identity and/or retrieve the identity data.

So I think the adaptor should expose operations like:

  • Exchange authorization code for tokens
  • Get UserInfo (identity claims)

@josephjclark

Copy link
Copy Markdown
Collaborator

Ok, so you use the adaptor to get an access token for other things, or to get information about a user.

How would we expect that token to be used downstream? I guess we have to write it to state to be used by other jobs. Not ideal because that's presumably a sensitive token and we wouldn't have any protection for it. It'll appear in dataclips and logs and won't even be hidden by the UI scrubber. And you wouldn't try to extract it into a credential somehow.

Do you have any more details about usage of these tokens? Would they have an expiry? Are there other auth systems that can be used? Maybe all the security stuff lately has got to my head, but it doesn't feel like we have a particualrly secure way of handling this right now. I can think of features that would help, but that's for down the road 🤔

@redietr

redietr commented Jul 14, 2026

Copy link
Copy Markdown

I think the flow would be something like this:

The user clicks Login with Fayda in the source app, enters their Fayda ID, and authenticates using OTP or biometrics. Once that works, Fayda redirects them back with an authorization code.

Then I guess the source app passes that code to OpenFn, and the adaptor handles the rest in one go. It exchanges the code for the access token, uses the token to call UserInfo, validates the response, and returns the identity information the workflow needs. So the adaptor would have a function like getUserInfo({ authorizationCode }).

So the token shouldn’t really need to be passed between jobs or written to state at all. It would just exist briefly inside that adaptor operation, and only the verified user details would move forward in the workflow.

@redietr

redietr commented Jul 14, 2026

Copy link
Copy Markdown

As for the expiry, the code and token both have very short expiry times (if my memory serves me correctly).

@redietr

redietr commented Jul 14, 2026

Copy link
Copy Markdown

@brandonjackson question for you. Why are we building a FAYDA adaptor instead of a MOSIP one? Fayda only serves Ethiopia whereas MOSIP would be applicable to anyone who wanted to use their id verification platform (e-signet).

// Sign a private_key_jwt client assertion for the token endpoint, as in
// the VeriFayda reference app. configuration.privateKey is a
// base64-encoded JWK.
export const generateClientAssertion = async configuration => {

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.

Is this based on the Feyda docs example? Looks very similar, with the exeption that the private key appears to be JSON for some reason in their docs

@IZO-Ong IZO-Ong Jul 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yup. I also based it on the oidc test app that Fayda provided.

Comment thread packages/fayda/src/Utils.js Outdated
@josephjclark

Copy link
Copy Markdown
Collaborator

@IZO-Ong Thank you for the responsive commit but please hold off further development until we've agreed exactly what we're building here :) This thread is largely speculative (but your work is already helping us to understand exactly what we need)

@redietr some more questions while I try to work this out:

Then I guess the source app passes that code to OpenFn

This must be through a webhook then? So the app calls the openfn webhook, passing the code through there.

So the token shouldn’t really need to be passed between jobs or written to state at all. It would just exist briefly inside that adaptor operation, and only the verified user details would move forward in the workflow.

That sounds pretty good. We can keep the jwt off state, and getAuthorizationUrl and getToken become private utility functions, rather than user facing operations.

So this is quite an unusual adaptor so it doesn't use a credential like most. It expects an authorisation code to be on input state, setup by whoever called it.

I keep skimming over the Fayda API docs but they're over my head really. This is stuff that only make sense if you already understand it!

@redietr

redietr commented Jul 14, 2026

Copy link
Copy Markdown

@josephjclark

  1. Yes webhook.
  2. i think you will still need a credential. Again, it's been a while but we do need a place to store client id, private key (decode the incoming token), token url (or at least the base url) etc .. I'm looking at this doc

@josephjclark

Copy link
Copy Markdown
Collaborator

Ah yes OK - I can see there is stuff we need to keep in the credential. Cool, so we can use keep that, and maybe add tokenExpirationTime @IZO-Ong

@IZO-Ong I think we've got enough clarity now that you can refactor a bit. See this comment:

We can keep the jwt off state, and getAuthorizationUrl and getToken become private utility functions, rather than user facing operations. The private stuff doesn't need full JSDoc.

So the only user facing operation will be getUserInfo(authorizationCode, options).

Finally @IZO-Ong have you been able to actually run anything against a live server here? Has anyone given you a credential and maybe some test data to fetch?

@IZO-Ong

IZO-Ong commented Jul 15, 2026

Copy link
Copy Markdown
Author

Noted on that @josephjclark, I will refactor along those lines.

As for testing, unfortunately I don't have any credentials or test data yet to run against a live server. Let me know if someone can share those!

@brandonjackson

Copy link
Copy Markdown
Contributor

@redietr I agree, I think we should explore whether Fayda could be served with a generic MOSIP or eSignet adaptor. It seems like the token generation step is useful but ultimately the getUserInfo function requires MOSIP. @IZO-Ong do you have thoughts on possible adaptor architectures here? eg

  • Fayda workflow uses 1 Fayda adaptor = bit of esignet + bit of mosip
  • Fayda workflow uses 2 separate adaptors... generic eSignet + generic MOSIP
  • something else?

For reference attaching a doc from the MOSIP conference I went to which shows use cases very clearly
DPGs Crossplatform Interoperability Webinar - Demo Scenarios.docx (1).pdf

@IZO-Ong

IZO-Ong commented Jul 18, 2026

Copy link
Copy Markdown
Author

Hi all, I've refactored the adaptor as per @josephjclark's notes. The adaptor now exposes a single operation getUserInfo(authorizationCode, options), and the client-assertion lifetime is configurable via configuration.tokenExpirationTime (default 5m).

@IZO-Ong

IZO-Ong commented Jul 18, 2026

Copy link
Copy Markdown
Author

@brandonjackson currently the Fayda adaptor can be treated like a generic eSignet / OIDC adaptor. eSignet is MOSIP's OIDC module, and getUserInfo just runs the standard authorise -> token -> userinfo flow to authenticate a person and return their verified KYC claims, so it does not directly call any MOSIP API; the same code will work for any MOSIP-based programme that uses eSignet.

However, from my understanding, MOSIP does have other functionalities (ID-Auth matching, credential issuance, record updates) that are separate and would be its own generic MOSIP adaptor, so I do lean towards the design paradigm of option 2 (generic eSignet adaptor + generic MOSIP adaptor), and this adaptor can be renamed as an eSignet adaptor.

@IZO-Ong
IZO-Ong requested a review from josephjclark July 18, 2026 07:05
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.

5 participants