Inji Certify (Pre-Auth) โ direct-to-PDF implementation guide
Version: Inji Certify v0.14.0 (
injistack/inji-certify-with-plugins:0.14.0);
inji-preauth-proxyv0.1.0 (adammwaniki/inji-preauth-poc:v0.1.0, built from
cmd/inji-preauth-proxy/).
Adapter type:inji_certify_preauthโinternal/adapters/injicertify.
Role: issuer only (no holder wallet โ the PDF is the credential).
The pre-auth path is the "registry-as-record / paper QR" model: an operator enters (or CSV-loads)
claims, verifiably runs the whole OID4VCI pre-authorized_code dance server-side, and hands back
a printable PDF whose QR embeds a signed VC. The subject walks away with paper, not a wallet
credential. It is a completely separate stack from
Inji Certify Auth-Code โ its own containers, DB, DID, and public host,
so the two issuers' keys never cross.
1. What it is
A second Inji Certify instance running the csvdp-farmer (CSV/pre-auth data-provider) profile.
Certify itself is its own OAuth authorization server here (it mints the pre-auth token โ no
eSignet). verifiably drives it, encodes the resulting VC as a MOSIP PixelPass QR, and renders a
one-page PDF.
Formats: ldp_vc (Ed25519Signature2020). DirectPDF: true.
2. The role verifiably plays โ server-side OID4VCI client + PDF renderer
verifiably runs the entire pre-auth dance in internal/adapters/injicertify/pdf.go
(issueAsPDFPreAuth) โ the subject types nothing and holds no wallet:
POST /v1/certify/pre-authorized-data(stage the offer with the claims).- Extract the pre-authorized code from the offer JSON.
POST /v1/certify/oauth/token(grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code)
โ access token +c_nonce.- Build an adapter-held ES256 proof JWT (P-256, header
{typ,alg,jwk}, nokid/issper
OID4VCI ยง7.2.1.1 anonymous pre-auth;aud= the offer'scredential_issuer). POST /v1/certify/issuance/credentialโ signed VC.encodePixelPass(vc)โ base45(zlib(CBOR(json)));renderCredentialPDFlays out claims + a
high-recovery QR; bytes stashed on the adapter, served atGET /issuer/issue/pdf/{id}.
verifiably also observes signing kids (injidid.Preauth.Remember) and serves the pre-auth
did.json patched with them, independent of the primary instance.
3. Services (compose)
| Service | Image | Port | Role |
|---|---|---|---|
inji-preauth-proxy (container inji-certify-preauth) |
adammwaniki/inji-preauth-poc:v0.1.0 |
8094:8090 |
Transparent proxy at Inji's network identity โ see ยง7. |
inji-certify-preauth-backend |
injistack/inji-certify-with-plugins:0.14.0 |
โ | The Certify pre-auth issuer. Profile default,csvdp-farmer. |
certify-preauth-nginx |
nginx:1.27 |
8095:80 |
Fronts the pre-auth backend; routes credential + well-known through verifiably-go (inji-proxy-preauth). |
certify-preauth-postgres |
postgres:15 |
5436:5432 |
DB inji_certify (isolated from the auth-code DB). |
The pre-auth data provider stashes claims in Certify's own cache during the
pre-authorized-data POST โ it does not use vc_subject or identity_registry.
4. Environment variables
inji-certify-preauth-backend (compose):
| Var | Value | Meaning |
|---|---|---|
active_profile_env |
default,csvdp-farmer |
Loads certify-default.properties + certify-csvdp-farmer-preauth.properties. |
CERTIFY_ISSUER_DID |
did:web:${PREAUTH_DID_DOMAIN:-${ISSUER_DID_DOMAIN:-certify-preauth-nginx}} |
Pre-auth issuer DID, decoupled from the auth-code instance. |
mosip_certify_domain_url |
${PREAUTH_PUBLIC_URL:-http://inji-certify-preauth:8090} |
Public โ so credential_issuer/credential_endpoint in offers + the DID are externally resolvable. |
MOSIP_CERTIFY_AUTHN_JWK_SET_URI |
${PREAUTH_JWKS_URI:-http://inji-certify-preauth:8090/โฆ/jwks.json} |
JWKS stays internal (cluster-local). |
SPRING_DATASOURCE_URL |
jdbc:postgresql://certify-preauth-postgres:5432/inji_certify |
Isolated DB. |
Derived in deploy.sh cmd_up: PREAUTH_PUBLIC_URL (url_for inji-certify-preauth, port 8094)
โ PREAUTH_DID_DOMAIN (host part). Unset host โ certify-preauth-nginx (dev). These are
deliberately separate from AUTHCODE_PUBLIC_URL/ISSUER_DID_DOMAIN โ re-pointing the shared
var would move the auth-code issuer onto the pre-auth did.json (which carries only the pre-auth
key) and break auth-code verification.
Adapter config (backends.json): baseUrl / publicBaseUrl =
https://inji-certify-preauth.in-labs.cdpi.dev, internalBaseUrl / offerIssuerUrl =
http://inji-certify-preauth:8090.
5. Caddy
inji-certify-preauth.in-labs.cdpi.dev {
reverse_proxy certify-preauth-nginx:80
}
A plain reverse proxy โ no did.json interception here (the pre-auth instance's did.json is served
through its own nginx โ verifiably-go inji-proxy-preauth). Hairpin: caddy-public aliases
inji-certify-preauth.<domain> so the wallet-api (walt.id) can resolve a pre-auth
credential_offer_uri whose host is the public pre-auth name without hairpinning the box IP.
6. Runtime configs
deploy/compose/stack/inji/certify/certify-default.propertiesโ shared with auth-code; the
pre-auth instance leavesAUTHCODE_ALLOWED_AUDunset, soauthn.allowed-audienceskeeps the
${mosip.certify.domain.url}default (certify's own token, aud = its public host).deploy/compose/stack/inji/certify/certify-csvdp-farmer-preauth.propertiesโ the pre-auth /
CSV data-provider plugin config.deploy/compose/stack/inji/certify-preauth-nginx/nginx.confโ same resolver+variable pattern as
certify-nginx; routes the credential endpoint + well-known through
verifiably-go/inji-proxy-preauth/*.deploy/compose/stack/inji/certify/init-preauth.sql+init-preauth.shโ seed the isolated DB.
7. The inji-preauth-proxy (why it exists)
Strict OID4VCI wallets (walt.id, Sphereon) follow the issuer identifier verbatim. Inji advertises
credential_issuer: http://inji-certify-preauth:8090 but serves metadata under
/v1/certify/issuance/.well-known/โฆ, not at the root โ so strict wallets 404. The proxy
(cmd/inji-preauth-proxy/main.go) sits at Inji's network identity and:
- answers
GET /.well-known/openid-credential-issuerat the root, proxying to Inji's real
metadata URL and rewriting the payload (strips null optionals, emptycredential_definition,
walt.id-incompatibledisplay) so strict JSON parsers accept it; - forwards the credential request through.
This is what let SD-JWT โ an external Credo mobile wallet work for pre-auth; walt.id's own
holder cannot claim pre-auth (v0.18.2 emits a non-conformant proof) โ use an external wallet + PDF.
8. Databases & APIs
Database inji_certify on certify-preauth-postgres (isolated). Uses credential_config
(custom schemas created via the issuer builder); not vc_subject/identity_registry (the
pre-auth data-provider caches claims itself).
Upstream API (via the proxy/nginx): /v1/certify/pre-authorized-data,
/v1/certify/oauth/token, /v1/certify/issuance/credential,
/v1/certify/issuance/.well-known/openid-credential-issuer.
verifiably surface: POST /issuer/issue (mode = "As a PDF"), GET /issuer/issue/pdf/{id},
plus the delegated-access pre-auth APIs /api/v1/delegation/inji/preauth/{issue,claim}.
Adapter files: internal/adapters/injicertify/pdf.go, pixelpass.go, export.go;
internal/handlers/pdf_download.go; internal/injidid/observed.go (Preauth).
9. Deploy + verify
ssh colombo 'cd /root/verifiably/verifiably-go && ./deploy.sh up inji' # brings up both certify instances
inji-preauth-proxy is built from source on first up (image adammwaniki/inji-preauth-poc:v0.1.0).
Smoke: /issuer/issue with an Inji Pre-Auth DPG + "As a PDF" โ a PDF downloads; rasterize the
QR and upload to Inji Verify โ SUCCESS.
10. Gotchas
- Keep it separate from auth-code. Distinct DB (
certify-preauth-postgres), DID
(did:web:inji-certify-preauth.<domain>), and did.json (only the pre-auth key). Never point
ISSUER_DID_DOMAINat the pre-auth host. - JWKS is internal on purpose.
MOSIP_CERTIFY_AUTHN_JWK_SET_URIstays cluster-local; only the
domain_url (issuer/offer URLs) goes public. - walt.id holder can't claim pre-auth. v0.18.2 emits a non-conformant proof (kid+jwk) and
PUT /credentialsis NotImplemented โ use an external wallet + SD-JWT, or the PDF path.
See also: Inji Certify Auth-Code ยท Inji Verify ยท
../architecture.md ยท ../deploy.md.