From e698546dc2c3db9427bc2ecef8540b98edb61abd Mon Sep 17 00:00:00 2001 From: Allan Kimmer Jensen Date: Fri, 10 Apr 2026 11:06:10 +0200 Subject: [PATCH 1/3] feat: output JSON from login, capture response body with tokens login command now outputs structured JSON to stdout with cookies, response body (when JSON), finalUrl, and provider. Progress messages go to stderr so piping works cleanly (e.g. | jq '.body.access_token'). Services returning tokens as JSON body instead of cookies are now captured in the output. Closes #8 --- README.md | 38 +++++++++++++++++++------ src/cli.ts | 78 ++++++++++++++++++++++++++++++++-------------------- src/login.ts | 2 ++ 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 1c1eb8a..77e5e2e 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ When MitID asks you to approve in the app, the CLI does it automatically via the ### Fully automated login -No browser needed. Gets session cookies you can use with curl, fetch, Playwright, etc: +No browser needed. Outputs JSON to stdout with cookies, tokens, and metadata. Progress goes to stderr so piping works cleanly: ```bash # Terminal 1: start the login @@ -61,16 +61,35 @@ mitid login myuser https://your-service.example.com/login/mitid mitid approve myuser ``` -The login command outputs session cookies and copies them to clipboard. +Output is JSON: + +```json +{ + "provider": "Criipto", + "finalUrl": "https://your-service.example.com/callback", + "cookies": { "session": "abc123", "token": "eyJ..." }, + "body": { "access_token": "eyJ...", "refresh_token": "..." } +} +``` + +The `body` field is included when the final response returns JSON (e.g. API token endpoints). Extract what you need with `jq`: + +```bash +# Get an access token +mitid login myuser | jq -r '.body.access_token' + +# Get cookies as a string for curl +mitid login myuser | jq -r '.cookies | to_entries | map("\(.key)=\(.value)") | join("; ")' +``` ### AI agent / browser automation For AI agents (Claude, Cursor, etc.) controlling a browser via Chrome DevTools MCP, Playwright, or similar; where the MitID widget refuses to render: -1. Run `mitid login ` to get session cookies +1. Run `mitid login ` to get JSON output 2. Run `mitid approve ` in parallel to auto-approve -3. Inject the cookies into the automated browser -4. Navigate to the service; you're logged in +3. Parse the JSON for cookies or access tokens +4. Inject the cookies into the automated browser, or use the access token as a Bearer token ```javascript // Example: inject cookies into an automated browser @@ -94,7 +113,7 @@ Prints detailed workflow instructions for all use cases including library usage. | Command | Description | |---------|-------------| | `mitid info ` | Show identity details (username, UUID, CPR, authenticators) | -| `mitid login ` | Complete a full MitID login and output session cookies | +| `mitid login ` | Complete a full MitID login and output JSON (cookies, tokens, metadata) | | `mitid approve ` | Poll and auto-approve a pending MitID login via the simulator. Use `--watch` to keep approving | | `mitid save [alias]` | Save an identity for quick access. Use `--note` to annotate | | `mitid list` | Show all saved identities | @@ -131,13 +150,14 @@ import { MitIDClient, login, approve, resolve } from '@saturate/mitid'; const { identity, codeApp } = await resolve('TestUser123'); console.log(identity.identityName, identity.cprNumber); -// Full login flow (returns session cookies) +// Full login flow (returns cookies, response body, and metadata) const result = await login( 'TestUser123', 'https://your-service.example.com/login/mitid', console.log // status callback ); -console.log(result.cookies); +console.log(result.cookies); // session cookies +console.log(result.body); // response body (may contain tokens) // Auto-approve a pending login await approve(identity.identityId, codeApp.authenticatorId); @@ -171,7 +191,7 @@ Service login URL → Extract "aux" from broker page → MitID core API: identify user → APP auth (push to simulator) → Poll for approval → SRP-6a key exchange → Finalize - → Authorization code → Broker callback → Session cookies + → Authorization code → Broker callback → Session cookies / tokens ``` The `aux` (auxiliary data) is a base64-encoded JSON blob that the broker passes to the MitID widget. It contains the `authenticationSessionId` and a `checksum` needed to start the authentication. Each broker delivers it differently (JSON endpoint, inline JS, POST response), which is why providers need different extraction logic. diff --git a/src/cli.ts b/src/cli.ts index 56a726f..f5dc233 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -145,10 +145,26 @@ const approveCmd = defineCommand({ }, }); +function stderr(msg: string): void { + process.stderr.write(`${msg}\n`); +} + +function tryParseJson(body: string): Record | null { + try { + const parsed: unknown = JSON.parse(body); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) + return parsed as Record; + } catch { + // not JSON + } + return null; +} + const loginCmd = defineCommand({ meta: { name: "login", - description: "Complete a full MitID login and output session cookies", + description: + "Complete a full MitID login and output JSON with cookies and tokens", }, args: { query: queryArg, @@ -163,32 +179,25 @@ const loginCmd = defineCommand({ async run({ args }) { const serviceUrl = args.url; - console.log( - `Logging in as ${args.query} to ${new URL(serviceUrl).hostname}...`, - ); - console.log( + stderr(`Logging in as ${args.query} to ${new URL(serviceUrl).hostname}...`); + stderr( `Run 'mitid approve ${args.query}' in another terminal to auto-approve.\n`, ); - const result = await login( - resolveQuery(args.query), - serviceUrl, - console.log, - ); + const result = await login(resolveQuery(args.query), serviceUrl, stderr); - if (result.cookies) { - console.log("\nSession cookies:"); - for (const [k, v] of Object.entries(result.cookies)) { - if (v) console.log(` ${k}=${v.substring(0, 50)}...`); - } - const cookieStr = Object.entries(result.cookies) - .filter(([, v]) => v) - .map(([k, v]) => `${k}=${v}`) - .join("; "); - if (copyToClipboard(cookieStr)) { - console.log("\nCookies copied to clipboard"); - } + const output: Record = { + provider: result.provider, + finalUrl: result.finalUrl, + cookies: result.cookies, + }; + + const json = tryParseJson(result.body); + if (json) { + output.body = json; } + + console.log(JSON.stringify(output, null, 2)); }, }); @@ -424,9 +433,10 @@ WORKFLOW 1: Manual browser testing 4. The CLI auto-approves The browser completes the login 5. Repeat as needed --watch keeps approving every login -WORKFLOW 2: Fully automated login (get session cookies) -------------------------------------------------------- - No browser needed. Gets session cookies you can use with curl, Playwright, etc. +WORKFLOW 2: Fully automated login +---------------------------------- + No browser needed. Outputs JSON to stdout with cookies, tokens, and metadata. + Progress messages go to stderr, so piping works cleanly. # Terminal 1: start the login mitid login https://your-service.example.com/login/mitid @@ -434,7 +444,14 @@ WORKFLOW 2: Fully automated login (get session cookies) # Terminal 2: auto-approve when it says "Waiting for MitID app approval" mitid approve - The login command outputs session cookies and copies them to clipboard. + # Output is JSON: + # { "provider": "...", "finalUrl": "...", "cookies": {...}, "body": {...} } + + # Extract a specific token: + mitid login | jq -r '.body.access_token' + + # Copy cookies to clipboard: + mitid login | jq -r '.cookies | to_entries | map("\\(.key)=\\(.value)") | join("; ")' | pbcopy WORKFLOW 3: AI agent with browser automation (Chrome MCP, Playwright, etc.) --------------------------------------------------------------------------- @@ -443,7 +460,7 @@ WORKFLOW 3: AI agent with browser automation (Chrome MCP, Playwright, etc.) 1. Run 'mitid login ' in background 2. Run 'mitid approve ' in parallel to auto-approve - 3. Capture the session cookies from the login output + 3. Parse the JSON output for cookies or access tokens 4. In the browser: navigate to the service URL 5. Inject cookies via JavaScript: document.cookie = "CookieName=value; path=/"; @@ -464,9 +481,10 @@ LIBRARY USAGE // Look up a test identity const { identity, codeApp } = await resolve('Username123'); - // Full login flow + // Full login flow — returns cookies, response body, and metadata const result = await login('Username123', 'https://service.example.com/login'); - console.log(result.cookies); + console.log(result.cookies); // session cookies + console.log(result.body); // response body (may contain tokens) // Or use the client directly const client = new MitIDClient('https://pp.mitid.dk'); @@ -491,7 +509,7 @@ HOW IT WORKS Service → OAuth redirect → Criipto/NemLog-in broker → MitID session → Identify user → APP auth (push to simulator) → Poll for approval → SRP key exchange → Finalize → Authorization code → Service callback - → Session cookies + → Session cookies / tokens `); }, diff --git a/src/login.ts b/src/login.ts index 0943380..3fca0f3 100644 --- a/src/login.ts +++ b/src/login.ts @@ -8,6 +8,7 @@ export type LoginStatusCallback = (message: string) => void; export interface LoginResult { cookies: CookieJar; + body: string; finalUrl: string; provider: string; } @@ -128,6 +129,7 @@ export async function login( return { cookies: final.cookies, + body: final.body, finalUrl: final.finalUrl, provider: provider.name, }; From 6bf2095f940744fa77436294c2d025d5c1769e5a Mon Sep 17 00:00:00 2001 From: Allan Kimmer Jensen Date: Fri, 10 Apr 2026 11:09:32 +0200 Subject: [PATCH 2/3] fix: always include body in login output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Body is now always present — parsed as JSON when possible, raw string otherwise. No data is lost regardless of response content type. --- src/cli.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f5dc233..47556f8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -190,13 +190,9 @@ const loginCmd = defineCommand({ provider: result.provider, finalUrl: result.finalUrl, cookies: result.cookies, + body: tryParseJson(result.body) ?? result.body, }; - const json = tryParseJson(result.body); - if (json) { - output.body = json; - } - console.log(JSON.stringify(output, null, 2)); }, }); From 43b90e22c37f54cedb83796e5824546c70c44176 Mon Sep 17 00:00:00 2001 From: Allan Kimmer Jensen Date: Fri, 10 Apr 2026 11:12:00 +0200 Subject: [PATCH 3/3] chore: fix lint warning, update docs for always-present body field - Remove unused private userId field in MitIDClient - Update README to reflect body is always present --- README.md | 2 +- src/cli.ts | 2 +- src/client.ts | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 77e5e2e..e525038 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Output is JSON: } ``` -The `body` field is included when the final response returns JSON (e.g. API token endpoints). Extract what you need with `jq`: +The `body` field is always present. Parsed as JSON when possible, raw string otherwise. Extract what you need with `jq`: ```bash # Get an access token diff --git a/src/cli.ts b/src/cli.ts index 47556f8..2aa7fd6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -477,7 +477,7 @@ LIBRARY USAGE // Look up a test identity const { identity, codeApp } = await resolve('Username123'); - // Full login flow — returns cookies, response body, and metadata + // Full login flow - returns cookies, response body, and metadata const result = await login('Username123', 'https://service.example.com/login'); console.log(result.cookies); // session cookies console.log(result.body); // response body (may contain tokens) diff --git a/src/client.ts b/src/client.ts index 1b9b23e..9e60ed7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -88,7 +88,6 @@ export class MitIDClient { private authenticatorSessionFlowKey!: string; private authenticatorEafeHash!: string; private authenticatorSessionId!: string; - private userId!: string; private finalizationSessionId: string | undefined; constructor(options: MitIDClientOptions | string = {}) { @@ -158,8 +157,6 @@ export class MitIDClient { } async identifyAndGetAuthenticators(userId: string): Promise { - this.userId = userId; - const idResp = await this.fetch( `${this.coreUrl}/v1/authentication-sessions/${this.authenticationSessionId}`, { method: "PUT", body: JSON.stringify({ identityClaim: userId }) },