Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <url> | jq -r '.body.access_token'

# Get cookies as a string for curl
mitid login myuser <url> | 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 <user> <service-login-url>` to get session cookies
1. Run `mitid login <user> <service-login-url>` to get JSON output
2. Run `mitid approve <user>` 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
Expand All @@ -94,7 +113,7 @@ Prints detailed workflow instructions for all use cases including library usage.
| Command | Description |
|---------|-------------|
| `mitid info <query>` | Show identity details (username, UUID, CPR, authenticators) |
| `mitid login <query> <url>` | Complete a full MitID login and output session cookies |
| `mitid login <query> <url>` | Complete a full MitID login and output JSON (cookies, tokens, metadata) |
| `mitid approve <query>` | Poll and auto-approve a pending MitID login via the simulator. Use `--watch` to keep approving |
| `mitid save <query> [alias]` | Save an identity for quick access. Use `--note` to annotate |
| `mitid list` | Show all saved identities |
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
76 changes: 45 additions & 31 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,26 @@ const approveCmd = defineCommand({
},
});

function stderr(msg: string): void {
process.stderr.write(`${msg}\n`);
}

function tryParseJson(body: string): Record<string, unknown> | null {
try {
const parsed: unknown = JSON.parse(body);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
return parsed as Record<string, unknown>;
} 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,
Expand All @@ -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<string, unknown> = {
provider: result.provider,
finalUrl: result.finalUrl,
cookies: result.cookies,
body: tryParseJson(result.body) ?? result.body,
};

console.log(JSON.stringify(output, null, 2));
},
});

Expand Down Expand Up @@ -468,17 +473,25 @@ 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 <alias> https://your-service.example.com/login/mitid

# Terminal 2: auto-approve when it says "Waiting for MitID app approval"
mitid approve <alias>

The login command outputs session cookies and copies them to clipboard.
# Output is JSON:
# { "provider": "...", "finalUrl": "...", "cookies": {...}, "body": {...} }

# Extract a specific token:
mitid login <alias> <url> | jq -r '.body.access_token'

# Copy cookies to clipboard:
mitid login <alias> <url> | jq -r '.cookies | to_entries | map("\\(.key)=\\(.value)") | join("; ")' | pbcopy

WORKFLOW 3: AI agent with browser automation (Chrome MCP, Playwright, etc.)
---------------------------------------------------------------------------
Expand All @@ -487,7 +500,7 @@ WORKFLOW 3: AI agent with browser automation (Chrome MCP, Playwright, etc.)

1. Run 'mitid login <user> <service-login-url>' in background
2. Run 'mitid approve <user>' 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=/";
Expand All @@ -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');
Expand All @@ -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

`);
},
Expand Down
3 changes: 0 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand Down Expand Up @@ -158,8 +157,6 @@ export class MitIDClient {
}

async identifyAndGetAuthenticators(userId: string): Promise<Authenticators> {
this.userId = userId;

const idResp = await this.fetch(
`${this.coreUrl}/v1/authentication-sessions/${this.authenticationSessionId}`,
{ method: "PUT", body: JSON.stringify({ identityClaim: userId }) },
Expand Down
2 changes: 2 additions & 0 deletions src/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type LoginStatusCallback = (message: string) => void;

export interface LoginResult {
cookies: CookieJar;
body: string;
finalUrl: string;
provider: string;
}
Expand Down Expand Up @@ -128,6 +129,7 @@ export async function login(

return {
cookies: final.cookies,
body: final.body,
finalUrl: final.finalUrl,
provider: provider.name,
};
Expand Down
Loading