Skip to content

Instantly share code, notes, and snippets.

@panva
Last active October 20, 2024 09:23

Revisions

  1. panva revised this gist Oct 20, 2024. 4 changed files with 6 additions and 10 deletions.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,5 +3,5 @@
    run

    ```sh
    npx --node-options '--experimental-strip-types' https://gist.github.com/panva/ebaacfe433a8677bdbf458f6e1132045
    npx https://gist.github.com/panva/ebaacfe433a8677bdbf458f6e1132045
    ```
    6 changes: 1 addition & 5 deletions dcr.mts → dcr.mjs
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,3 @@
    #!/usr/bin/env node

    import type { ClientMetadata } from "openid-client";

    const r = await fetch("https://op.panva.cz/reg", {
    method: "POST",
    headers: {
    @@ -16,6 +12,6 @@ const r = await fetch("https://op.panva.cz/reg", {
    }),
    });

    const { client_id } = (await r.json()) as ClientMetadata;
    const { client_id } = await r.json();

    export { client_id };
    4 changes: 2 additions & 2 deletions index.mts → index.mjs
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,7 @@

    import * as client from "openid-client";
    import open from "open";
    import { client_id } from "./dcr.mts";
    import { client_id } from "./dcr.mjs";

    const config = await client.discovery(
    new URL("https://op.panva.cz"),
    @@ -16,7 +16,7 @@ const deviceAuthorizationResponse = await client.initiateDeviceAuthorization(

    console.log("user code:", deviceAuthorizationResponse.user_code);

    await open(deviceAuthorizationResponse.verification_uri_complete!, {
    await open(deviceAuthorizationResponse.verification_uri_complete, {
    wait: false,
    });

    4 changes: 2 additions & 2 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -2,8 +2,8 @@
    "name": "pg",
    "version": "1.0.0",
    "description": "",
    "main": "index.mts",
    "bin": "./index.mts",
    "main": "index.mjs",
    "bin": "./index.mjs",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
  2. panva revised this gist Oct 20, 2024. 6 changed files with 274 additions and 43 deletions.
    6 changes: 3 additions & 3 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    # Simple Device Flow Login CLI implementation

    run
    run

    ```sh
    npx https://gist.github.com/panva/ebaacfe433a8677bdbf458f6e1132045
    ```
    npx --node-options '--experimental-strip-types' https://gist.github.com/panva/ebaacfe433a8677bdbf458f6e1132045
    ```
    21 changes: 21 additions & 0 deletions dcr.mts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    #!/usr/bin/env node

    import type { ClientMetadata } from "openid-client";

    const r = await fetch("https://op.panva.cz/reg", {
    method: "POST",
    headers: {
    "content-type": "application/json",
    },
    body: JSON.stringify({
    grant_types: ["urn:ietf:params:oauth:grant-type:device_code"],
    response_types: [],
    redirect_uris: [],
    token_endpoint_auth_method: "none",
    application_type: "native",
    }),
    });

    const { client_id } = (await r.json()) as ClientMetadata;

    export { client_id };
    36 changes: 0 additions & 36 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -1,36 +0,0 @@
    #!/usr/bin/env node

    /* eslint-disable no-console, camelcase */

    const { Issuer } = require('openid-client');
    const open = require('open');

    const { ISSUER = 'https://op.panva.cz' } = process.env;
    const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';

    (async () => {
    const issuer = await Issuer.discover(ISSUER);

    const client = await issuer.Client.register({
    grant_types: [GRANT_TYPE],
    response_types: [],
    redirect_uris: [],
    token_endpoint_auth_method: 'none',
    application_type: 'native',
    });

    const handle = await client.deviceAuthorization();

    await open(handle.verification_uri_complete, { wait: false });

    const tokenSet = await handle.poll();

    console.log('got', tokenSet);
    console.log('id token claims', tokenSet.claims());

    const userinfo = await client.userinfo(tokenSet);
    console.log('userinfo', userinfo);
    })().catch((err) => {
    console.error(err);
    process.exitCode = 1;
    });
    39 changes: 39 additions & 0 deletions index.mts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,39 @@
    #!/usr/bin/env node

    import * as client from "openid-client";
    import open from "open";
    import { client_id } from "./dcr.mts";

    const config = await client.discovery(
    new URL("https://op.panva.cz"),
    client_id,
    );

    const deviceAuthorizationResponse = await client.initiateDeviceAuthorization(
    config,
    { scope: "openid email" },
    );

    console.log("user code:", deviceAuthorizationResponse.user_code);

    await open(deviceAuthorizationResponse.verification_uri_complete!, {
    wait: false,
    });

    const tokens = await client.pollDeviceAuthorizationGrant(
    config,
    deviceAuthorizationResponse,
    );

    console.log("got", tokens);
    const idTokenClaims = tokens.claims();
    if (idTokenClaims) {
    console.log("id token claims", idTokenClaims);

    const userinfo = await client.fetchUserInfo(
    config,
    tokens.access_token,
    idTokenClaims.sub,
    );
    console.log("userinfo", userinfo);
    }
    204 changes: 204 additions & 0 deletions package-lock.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,204 @@
    {
    "name": "pg",
    "version": "1.0.0",
    "lockfileVersion": 3,
    "requires": true,
    "packages": {
    "": {
    "name": "pg",
    "version": "1.0.0",
    "license": "ISC",
    "dependencies": {
    "open": "^10.1.0",
    "openid-client": "^6.1.1"
    },
    "bin": {
    "pg": "index.js"
    },
    "devDependencies": {
    "@types/node": "^22.7.7"
    }
    },
    "node_modules/@types/node": {
    "version": "22.7.7",
    "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz",
    "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==",
    "dev": true,
    "license": "MIT",
    "dependencies": {
    "undici-types": "~6.19.2"
    }
    },
    "node_modules/bundle-name": {
    "version": "4.1.0",
    "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
    "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
    "license": "MIT",
    "dependencies": {
    "run-applescript": "^7.0.0"
    },
    "engines": {
    "node": ">=18"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/default-browser": {
    "version": "5.2.1",
    "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
    "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
    "license": "MIT",
    "dependencies": {
    "bundle-name": "^4.1.0",
    "default-browser-id": "^5.0.0"
    },
    "engines": {
    "node": ">=18"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/default-browser-id": {
    "version": "5.0.0",
    "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
    "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
    "license": "MIT",
    "engines": {
    "node": ">=18"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/define-lazy-prop": {
    "version": "3.0.0",
    "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
    "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
    "license": "MIT",
    "engines": {
    "node": ">=12"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/is-docker": {
    "version": "3.0.0",
    "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
    "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
    "license": "MIT",
    "bin": {
    "is-docker": "cli.js"
    },
    "engines": {
    "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/is-inside-container": {
    "version": "1.0.0",
    "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
    "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
    "license": "MIT",
    "dependencies": {
    "is-docker": "^3.0.0"
    },
    "bin": {
    "is-inside-container": "cli.js"
    },
    "engines": {
    "node": ">=14.16"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/is-wsl": {
    "version": "3.1.0",
    "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
    "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
    "license": "MIT",
    "dependencies": {
    "is-inside-container": "^1.0.0"
    },
    "engines": {
    "node": ">=16"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/jose": {
    "version": "5.9.4",
    "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.4.tgz",
    "integrity": "sha512-WBBl6au1qg6OHj67yCffCgFR3BADJBXN8MdRvCgJDuMv3driV2nHr7jdGvaKX9IolosAsn+M0XRArqLXUhyJHQ==",
    "license": "MIT",
    "funding": {
    "url": "https://github.com/sponsors/panva"
    }
    },
    "node_modules/oauth4webapi": {
    "version": "3.1.1",
    "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.1.tgz",
    "integrity": "sha512-0h4FZjsntbKQ5IHGM9mFT7uOwQCRdcTG7YhC0xXlWIcCch24wUa6Vggaipa3Sw6Ab7nEnmO4rctROmyuHBfP7Q==",
    "license": "MIT",
    "funding": {
    "url": "https://github.com/sponsors/panva"
    }
    },
    "node_modules/open": {
    "version": "10.1.0",
    "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
    "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
    "license": "MIT",
    "dependencies": {
    "default-browser": "^5.2.1",
    "define-lazy-prop": "^3.0.0",
    "is-inside-container": "^1.0.0",
    "is-wsl": "^3.1.0"
    },
    "engines": {
    "node": ">=18"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/openid-client": {
    "version": "6.1.1",
    "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.1.1.tgz",
    "integrity": "sha512-JVN2ao1ErxHdWTme+xAQrLsQV2rV4RlhdiR8ZfaN0ktMhE8lEapU+VH7XuHSxTPHaAK7Rz7AqIB98ea+cHaCUg==",
    "license": "MIT",
    "dependencies": {
    "jose": "^5.9.4",
    "oauth4webapi": "^3.1.1"
    },
    "funding": {
    "url": "https://github.com/sponsors/panva"
    }
    },
    "node_modules/run-applescript": {
    "version": "7.0.0",
    "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
    "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
    "license": "MIT",
    "engines": {
    "node": ">=18"
    },
    "funding": {
    "url": "https://github.com/sponsors/sindresorhus"
    }
    },
    "node_modules/undici-types": {
    "version": "6.19.8",
    "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
    "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
    "dev": true,
    "license": "MIT"
    }
    }
    }
    11 changes: 7 additions & 4 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -2,15 +2,18 @@
    "name": "pg",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "bin": "./index.js",
    "main": "index.mts",
    "bin": "./index.mts",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "open": "^7.0.0",
    "openid-client": "^3.8.3"
    "open": "^10.1.0",
    "openid-client": "^6.1.1"
    },
    "devDependencies": {
    "@types/node": "^22.7.7"
    }
    }
  3. panva revised this gist Apr 29, 2021. 3 changed files with 11 additions and 1 deletion.
    7 changes: 7 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    # Simple Device Flow Login CLI implementation

    run

    ```sh
    npx https://gist.github.com/panva/ebaacfe433a8677bdbf458f6e1132045
    ```
    2 changes: 2 additions & 0 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    #!/usr/bin/env node

    /* eslint-disable no-console, camelcase */

    const { Issuer } = require('openid-client');
    3 changes: 2 additions & 1 deletion package.json
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,8 @@
    "name": "pg",
    "version": "1.0.0",
    "description": "",
    "main": "codeflow.js",
    "main": "index.js",
    "bin": "./index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
  4. panva revised this gist Nov 22, 2019. 2 changed files with 6 additions and 6 deletions.
    4 changes: 2 additions & 2 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    /* eslint-disable no-console, camelcase */

    const { Issuer } = require('openid-client');
    const opn = require('opn');
    const open = require('open');

    const { ISSUER = 'https://op.panva.cz' } = process.env;
    const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
    @@ -19,7 +19,7 @@ const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';

    const handle = await client.deviceAuthorization();

    await opn(handle.verification_uri_complete, { wait: false });
    await open(handle.verification_uri_complete, { wait: false });

    const tokenSet = await handle.poll();

    8 changes: 4 additions & 4 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -1,15 +1,15 @@
    {
    "name": "example",
    "name": "pg",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "main": "codeflow.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "openid-client": "^3.8.3",
    "opn": "^5.3.0"
    "open": "^7.0.0",
    "openid-client": "^3.8.3"
    }
    }
  5. panva revised this gist Nov 22, 2019. 2 changed files with 6 additions and 44 deletions.
    46 changes: 5 additions & 41 deletions index.js
    Original file line number Diff line number Diff line change
    @@ -1,19 +1,13 @@
    /* eslint-disable no-console, camelcase */

    const { randomBytes, createHash } = require('crypto');

    const { encode } = require('base64url');
    const { post } = require('got');
    const { Issuer } = require('openid-client');
    const opn = require('opn');

    const { ISSUER = 'https://guarded-cliffs-8635.herokuapp.com' } = process.env;
    const { ISSUER = 'https://op.panva.cz' } = process.env;
    const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
    const SOFT_ERRORS = new Set(['authorization_pending', 'slow_down']);

    (async () => {
    const issuer = await Issuer.discover(ISSUER);
    const { device_authorization_endpoint } = issuer;

    const client = await issuer.Client.register({
    grant_types: [GRANT_TYPE],
    @@ -23,44 +17,14 @@ const SOFT_ERRORS = new Set(['authorization_pending', 'slow_down']);
    application_type: 'native',
    });

    const code_verifier = randomBytes(8).toString('hex');
    const code_challenge = encode(createHash('sha256').update(code_verifier).digest());

    const {
    body: { device_code, verification_uri_complete },
    } = await post(device_authorization_endpoint, {
    json: true,
    form: true,
    body: {
    client_id: client.client_id,
    scope: 'openid email',
    code_challenge,
    code_challenge_method: 'S256',
    claims: JSON.stringify({ id_token: { email: null } }),
    },
    });
    const handle = await client.deviceAuthorization();

    await opn(verification_uri_complete, { wait: false });
    await opn(handle.verification_uri_complete, { wait: false });

    const tokenSet = await new Promise((resolve, reject) => {
    function poll() {
    client.grant({
    grant_type: GRANT_TYPE,
    device_code,
    code_verifier,
    }).then(resolve, (err) => {
    if (err.name === 'OpenIdConnectError' && SOFT_ERRORS.has(err.error)) {
    setTimeout(poll, 3000);
    } else {
    reject(err);
    }
    });
    }
    setTimeout(poll, 3000);
    });
    const tokenSet = await handle.poll();

    console.log('got', tokenSet);
    console.log('id token claims', tokenSet.claims);
    console.log('id token claims', tokenSet.claims());

    const userinfo = await client.userinfo(tokenSet);
    console.log('userinfo', userinfo);
    4 changes: 1 addition & 3 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -9,9 +9,7 @@
    "author": "",
    "license": "ISC",
    "dependencies": {
    "base64url": "^3.0.0",
    "got": "^8.3.2",
    "openid-client": "^2.2.1",
    "openid-client": "^3.8.3",
    "opn": "^5.3.0"
    }
    }
  6. panva revised this gist Jul 23, 2018. 1 changed file with 17 additions and 0 deletions.
    17 changes: 17 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    {
    "name": "example",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "base64url": "^3.0.0",
    "got": "^8.3.2",
    "openid-client": "^2.2.1",
    "opn": "^5.3.0"
    }
    }
  7. panva renamed this gist Jul 23, 2018. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  8. panva created this gist Jul 23, 2018.
    70 changes: 70 additions & 0 deletions device_flow.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,70 @@
    /* eslint-disable no-console, camelcase */

    const { randomBytes, createHash } = require('crypto');

    const { encode } = require('base64url');
    const { post } = require('got');
    const { Issuer } = require('openid-client');
    const opn = require('opn');

    const { ISSUER = 'https://guarded-cliffs-8635.herokuapp.com' } = process.env;
    const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
    const SOFT_ERRORS = new Set(['authorization_pending', 'slow_down']);

    (async () => {
    const issuer = await Issuer.discover(ISSUER);
    const { device_authorization_endpoint } = issuer;

    const client = await issuer.Client.register({
    grant_types: [GRANT_TYPE],
    response_types: [],
    redirect_uris: [],
    token_endpoint_auth_method: 'none',
    application_type: 'native',
    });

    const code_verifier = randomBytes(8).toString('hex');
    const code_challenge = encode(createHash('sha256').update(code_verifier).digest());

    const {
    body: { device_code, verification_uri_complete },
    } = await post(device_authorization_endpoint, {
    json: true,
    form: true,
    body: {
    client_id: client.client_id,
    scope: 'openid email',
    code_challenge,
    code_challenge_method: 'S256',
    claims: JSON.stringify({ id_token: { email: null } }),
    },
    });

    await opn(verification_uri_complete, { wait: false });

    const tokenSet = await new Promise((resolve, reject) => {
    function poll() {
    client.grant({
    grant_type: GRANT_TYPE,
    device_code,
    code_verifier,
    }).then(resolve, (err) => {
    if (err.name === 'OpenIdConnectError' && SOFT_ERRORS.has(err.error)) {
    setTimeout(poll, 3000);
    } else {
    reject(err);
    }
    });
    }
    setTimeout(poll, 3000);
    });

    console.log('got', tokenSet);
    console.log('id token claims', tokenSet.claims);

    const userinfo = await client.userinfo(tokenSet);
    console.log('userinfo', userinfo);
    })().catch((err) => {
    console.error(err);
    process.exitCode = 1;
    });