npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aplinkosministerija/viisp-v3-oidc

v0.1.2

Published

Framework-agnostic TypeScript client for VIISP V3 (eVartai) OIDC citizen authentication. Handles the authorize leg, signed state cookie, private_key_jwt token exchange, id_token verification and identity mapping — leaving user provisioning and session iss

Readme

@aplinkosministerija/viisp-v3-oidc

Framework-agnostic TypeScript client for VIISP V3 (eVartai) OIDC citizen authentication — the Lithuanian national identity provider operated by the Information Society Development Committee.

Extracted from biip-alis-api so any BIIP application can add eVartai login without re-implementing the protocol.

What it does, and what it deliberately does not

It does the whole VIISP-facing protocol:

  • builds the authorize URL with state + nonce
  • seals the pending login into a signed, HttpOnly state cookie
  • verifies your signing key actually pairs with the certificate registered at VIISP
  • fetches and caches the discovery document + JWKS, and checks the issuer
  • exchanges the authorization code using private_key_jwt (RS256 client_assertion)
  • verifies the id_token — signature, issuer, audience, nonce, azp
  • maps VIISP claims onto a flat identity object

It does not know about your users, sessions, database, or HTTP framework. It hands you a verified identity; provisioning the account and issuing your own session stays in your application, where it belongs.

Install

npm install @aplinkosministerija/viisp-v3-oidc

Requires Node 18+ (uses the global fetch).

Usage

Configure once

import { buildV3Config } from '@aplinkosministerija/viisp-v3-oidc';

const cfg = buildV3Config({
  env: 'STAGE',                          // DEV | TEST | STAGE | PROD
  pid: process.env.VIISP_V3_PID!,        // your VIISP service PID = OAuth2 client_id
  allowedHosts: ['https://myapp.biip.lt'],
  stateCookieSecret: process.env.JWT_SECRET!,
  cookieSecure: process.env.NODE_ENV === 'production',
  keyPath: './keys/viisp-v3-private.pem',
  certPath: './keys/viisp-v3-public.crt',
});

configFromEnv({ stateCookieSecret }) is available if you would rather adopt the reference VIISP_V3_* variable names (see Configuration).

Leg 1 — start the login

import { beginLogin } from '@aplinkosministerija/viisp-v3-oidc';

const { authorizeUrl, setCookie } = beginLogin(cfg, { host: feOrigin });
res.setHeader('Set-Cookie', setCookie);
res.json({ url: authorizeUrl });   // FE navigates the browser here

Leg 2 — handle the callback

import { completeLogin, peekPendingLogin, V3Error } from '@aplinkosministerija/viisp-v3-oidc';

const stateCookie = parseCookies(req.headers.cookie)[cfg.stateCookieName ?? 'viisp_v3_state'];

try {
  const { identity, clearCookie } = await completeLogin(cfg, {
    code: req.query.code,
    state: req.query.state,
    stateCookie,
    logger: myLogger,
  });

  res.setHeader('Set-Cookie', [clearCookie, mySessionCookie(identity)]);
  // identity.personalCode is guaranteed present here — provision your user now.
} catch (err) {
  const feHost = peekPendingLogin(cfg, stateCookie)?.host ?? cfg.allowedHosts[0];
  if (err instanceof V3Error) { /* map to your own status + localised message */ }
  res.redirect(`${feHost}/login?error=login_failed`);
}

The flow

FE  ──POST /sign──►  beginLogin()            → authorizeUrl + state cookie
browser ────────────► VIISP authorize page
VIISP ──GET callback?code&state──►  completeLogin()
                                     ├─ verify state cookie
                                     ├─ key/cert preflight
                                     ├─ discovery + issuer check
                                     ├─ token exchange (private_key_jwt)
                                     ├─ verify id_token (nonce, azp, …)
                                     └─ map claims → EvData
your app ──────────► create/find user, issue session, redirect to FE

Configuration

| Option | Required | Notes | |---|---|---| | env | yes | DEV / TEST / STAGE / PROD — selects the ap*.epaslaugos.lt host | | pid | yes | VIISP service PID; used as client_id and expected aud | | allowedHosts | yes | FE origins permitted to start a login | | stateCookieSecret | yes | HMAC secret for the state cookie | | cookieSecure | yes | emit Secure; false only for plain-HTTP local dev | | privateKeyPem / keyPath | one of | PKCS#8 key signing client_assertion | | certBytes / certPath | one of | registered client certificate (DER .cer or PEM) | | redirectUri | no | full override; otherwise derived from the FE origin | | redirectPath | no | default /api/auth/evartai/v3/callback | | offlineAccess | no | request offline_access for a refresh token | | stateCookieName | no | default viisp_v3_state | | authorizeUrl / authorizeBase | no | when AP fronts authorize elsewhere |

configFromEnv() reads VIISP_V3_ENV, VIISP_V3_PID, VIISP_ALLOWED_HOSTS, VIISP_V3_PRIVATE_KEY, VIISP_V3_KEY_PATH, VIISP_V3_CERT_PATH, VIISP_V3_REDIRECT_URI, VIISP_V3_OFFLINE_ACCESS, VIISP_V3_AUTHORIZE_URL, VIISP_V3_AUTHORIZE_BASE. Anything you pass explicitly wins. An omitted VIISP_V3_ENV defaults to STAGE; an unrecognised non-empty value throws instead of silently routing authentication to the wrong environment.

Errors

All extend V3Error, so catch (e) { if (e instanceof V3Error) … } works.

| Error | Meaning | Suggested status | |---|---|---| | V3HostNotAllowedError | origin not allow-listed — possible callback hijack | 400 | | V3StateMismatchError | state cookie missing/expired/tampered, or state differs | 401 | | V3ConfigError | cert unreadable, or key/cert do not pair | 500 | | V3DiscoveryError | discovery unreachable or issuer mismatch | 502 | | V3TokenExchangeError | token endpoint rejected the exchange | 502 | | V3IdTokenError | signature / issuer / audience / nonce / azp failure | 401 | | V3IdentityIncompleteError | verified, but no personal code present | 401 |

Security notes

  • redirect_uri comes from the signed state cookie, never re-derived from the callback request — the two legs must match byte-for-byte, and the callback is attacker-influenced.
  • SameSite=Lax on the state cookie is required, not an oversight: the VIISP callback is a top-level cross-site GET, and Strict would withhold the cookie and break every login.
  • State cookies fail closed. Missing field, wrong algorithm, bad signature, expiry → null, never a partially-trusted object.
  • Only one login can be pending per cookie name. Starting another login in the same browser overwrites the earlier state cookie; use distinct stateCookieName values for independently configured concurrent flows.
  • The key/cert preflight refuses the exchange on mismatch. Otherwise VIISP answers with an opaque "Could not validate signature" that gives no hint the cause is local.
  • Personal codes are never logged. When userData and sub disagree the library warns that they diverged, without emitting either value.
  • The library never logs the authorization code, the client_assertion, or the private key.

The sub fallback

VIISP normally returns the personal code in userData.authenticationAttribute. On TEST (VSID000000000115-demo) it has been observed to arrive only in the top-level sub claim, formatted <kodas>:<reikšmė>. The mapper prefers the explicit attribute and falls back to sub, accepting only recognised personal-code prefixes (lt-personal-code, iltu-personal-code, eidas-eid) so a company-only sub can never masquerade as a personal identifier.

Development

npm install
npm test
npm run build

License

MIT