diff --git a/README.md b/README.md index 1c1eb8a..e525038 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 always present. Parsed as JSON when possible, raw string otherwise. 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 3af140c..1fe7840 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -179,10 +179,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, @@ -197,32 +213,21 @@ 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, + body: tryParseJson(result.body) ?? result.body, + }; + + console.log(JSON.stringify(output, null, 2)); }, }); @@ -468,9 +473,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 @@ -478,7 +484,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.) --------------------------------------------------------------------------- @@ -487,7 +500,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=/"; @@ -508,9 +521,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'); @@ -535,7 +549,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/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 }) }, 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, };