oauth2-forwarder 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Sam Davidoff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # oauth2-forwarder
2
+
3
+ This utility allows forwarding an oauth2 interactive request that is initiated inside a docker container to the browser on the docker host and then forwarding the resulting redirect back into the docker container. The net effect is that a command line tool inside the docker container can perform interactive oauth2 authentication using the browser flow on its host.
4
+
5
+ ## Background
6
+
7
+ Imagine you have a command line tool that performs oauth2 interactive login. The typical flow of that application is this:
8
+
9
+ 1. CLI tool sends an interactive login url to the browser and, simultaneously, opens up an http service to listen for the redirect response.
10
+ 2. The browser opens the login url and the user performs interactive authentication.
11
+ 3. The successful interactive authentication results in a redirect GET request being made with the new authentication code.
12
+ 4. The http service setup by the CLI tool receives the redirect requests, extracts the code, and uses it.
13
+
14
+ This, for example, is exactly how the @azure/msal-node library works if you use the `.acquireTokenInteractive()` method on a public application.
15
+
16
+ This flow breaks down if you try to use it inside a docker container (at least one without a GUI) because step 2 fails, i.e., there is no way for the CLI to launch the host's browser for interactive login. You could get around this by using a device flow, but (a) that's a little more cumbersome, and (b) often conditional access policies will block that flow if they are checking for device compliance.
17
+
18
+ If you ever tried to do this from inside a docker container managed by VS Code, you'd have seen that it magically works. This utility gives you similar functionality without needing to use VS Code.
19
+
20
+ This utility works by changing the flow above as follows using a client-server pair provided by the utility--called, respectively, `o2f-server` and `o2f-client`--to proxy sending the request and redirect urls back and forth between the container and host. The flow then becomes:
21
+
22
+ 1. [container] CLI tool sends an interactive login url to the browser and, simultaneously, opens up an http service to listen for the redirect response.
23
+ 2. [container] `o2f-client` intercepts the request and forwards the url to `o2f-server` over a custom tcp port
24
+ 3. [host] `o2f-server` receives the request sends the the login url to the browser and simultaneously opens an http service to listen for the redirect response
25
+ 4. [host] The browser opens the login url and the user performs interactive authentication.
26
+ 5. [host] The successful interactive authentication results in a redirect GET request being made with the new authentication code.
27
+ 6. [host] The http service setup by `o2f-server` receives the redirect requests, extracts the code, and sends it back to `o2f-client` using the tcp channel opened in step 2.
28
+ 7. [container] `o2f-client` makes a GET requested for the redirect url it just received.
29
+ 8. [container] The http service setup by the CLI tool receives the redirect requests, extracts the code, and uses it.
30
+
31
+ ## Installation and Usage
32
+
33
+ This helper is written in Typescript and compiles down to two Javascript scripts, one for the server and one for the client.
34
+
35
+ ### Option 1: Install via npm (Recommended)
36
+
37
+ You can install oauth2-forwarder globally via npm:
38
+
39
+ ```bash
40
+ npm install -g oauth2-forwarder
41
+ ```
42
+
43
+ After installation, you'll have the following commands available globally:
44
+ - `o2f-server` - Run on the host machine
45
+ - `o2f-client` - Run on the container
46
+ - `o2f-browser` - Browser script for the container
47
+
48
+ #### On the host
49
+
50
+ Run `o2f-server` on the host machine. This will start the server and display the port it's listening on.
51
+
52
+ #### In the container
53
+
54
+ 1. Set the server info environment variable based on the output from the host:
55
+ ```bash
56
+ export OAUTH2_FORWARDER_SERVER="host.docker.internal:PORT"
57
+ ```
58
+ where PORT is the port displayed when you ran `o2f-server`.
59
+
60
+ 2. Set the BROWSER environment variable to use the browser script:
61
+ ```bash
62
+ export BROWSER=o2f-browser
63
+ ```
64
+
65
+ ### Option 2: Download Manually
66
+
67
+ Download the latest release from this repo. The release consists of a file named `oauth2-forwarder.zip` which contains two Javascript scripts: `o2f-server.js` and `o2f-client.js` plus a helper `browser.sh` script, all in a directory called `o2f`. These can be placed wherever you want, but these instructions assume they are placed in the home directories of the host and container.
68
+
69
+ ### On the host
70
+
71
+ Run `node ~/of2-server.js`. This will launch the server and it will listen for TCP connections on localhost at a random port which will be displayed in the console. You will need to keep this console/terminal open.
72
+
73
+ Notes:
74
+
75
+ - You can tell it to use a specific port by setting the environmental variable `OAUTH2_FORWARDER_PORT`
76
+
77
+ ### In the container
78
+
79
+ Run `export OAUTH2_FORWARDER_SERVER="host.docker.internal:PORT` where PORT is replaced with the port displayed when you ran the server.
80
+
81
+ Run `export BROWSER=~/o2f/browser.sh` to set an environmental variable that will intercept attemps to open the system browser. If you've moved the `o2f` directory you'll need to change this variable setting appropriately.
82
+
83
+ NB: This works for a linux container that uses the BROWSER variable to determine how to handle requests to open things in a browser, for example, like those created by the npm `open` library. If you have a different setup your mileage my vary. Feel free to raise an issue here to get help.
84
+
85
+ Run your oauth2 CLI app as normal.
86
+
87
+ Notes:
88
+
89
+ - You can turn on more verbose debugging information by setting the environmental variable `OAUTH2_FORWARDER_DEBUG` to `true`. The logging on the client side is saved in `/tmp/oauth2-forwarder.log`. On the server side it is output to the console.
90
+
91
+ ### Using a Dockerfile
92
+
93
+ Here's a strategy to make this fairly easy to use with a Docker container built with a Dockerfile.
94
+
95
+ #### Option 1: Using npm (Recommended)
96
+
97
+ On the host, set a specific port that you will listen on by configuring the env variable `OAUTH2_FORWARDER_PORT`.
98
+
99
+ Add these lines in the Dockerfile:
100
+
101
+ ```
102
+ RUN npm install -g oauth2-forwarder
103
+ ENV OAUTH2_FORWARDER_SERVER host.docker.internal:[PORT]
104
+ ENV BROWSER o2f-browser
105
+ ```
106
+
107
+ Replace `[PORT]` with the actual port number (or use Docker's `ARG` command).
108
+
109
+ #### Option 2: Using the zip release
110
+
111
+ On the host, set a specific port that you will listen on by configuring the env variable `OAUTH2_FORWARDER_PORT`.
112
+
113
+ Add these lines in the Dockerfile:
114
+
115
+ ```
116
+ RUN curl -LO https://github.com/sam-mfb/oauth2-forwarder/releases/download/v[VERSION]/oauth2-forwarder.zip
117
+ RUN unzip oauth2-forwarder.zip
118
+ ENV OAUTH2_FORWARDER_SERVER host.docker.internal:[PORT]
119
+ ENV BROWSER ~/o2f/browser.sh
120
+ ```
121
+
122
+ Of course, replace `[VERSION]` and `[PORT]` with the actual version number and port number (or use Docker's `ARG` command).
123
+
124
+ ## Debugging
125
+
126
+ You can enable debugging on either the server or the client by setting the environmental variable `OAUTH2_FORWARDER_DEBUG` to `true`.
127
+
128
+ ## Security
129
+
130
+ Since this is using all localhost tcp communications the security model is the same using this tool as it is in the non-containerized solution. In other words, in both cases the received auth code is transmitted over plaintext tcp on the localhost only. NB: you could modify this tool to send the client<-->server traffic across the network, but that would not be a good idea.
131
+
132
+ NB: If you believe there is a security issue with the app, please reach out to me directly via email, which is just `sam` at my company's domain.
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Assing this script to the BROWSER env variable via:
4
+ # export BROWSER=o2f-browser
5
+ #
6
+ # This will ensure requests to open a browser get forwarded
7
+ # through to the proxy
8
+ #
9
+ LOG_FILE="/tmp/oauth2-forwarder.log"
10
+
11
+ # fork and return 0 as some apps (e.g., az cli) expect a zero return
12
+ # before they will launch their redirect listener
13
+ o2f-client "$@" >> "$LOG_FILE" 2>&1 &
14
+
15
+ exit 0
package/browser.sh ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Assing this script to the BROWSER env variable via:
4
+ # export BROWSER=~/o2f/browser.sh
5
+ #
6
+ # This will ensure requests to open a browser get forwarded
7
+ # through to the proxy
8
+ #
9
+ LOG_FILE="/tmp/oauth2-forwarder.log"
10
+
11
+ # fork and return 0 as some apps (e.g., az cli) expect a zero return
12
+ # before they will launch their redirect listener
13
+ node ~/o2f/o2f-client.js "$@" >> "$LOG_FILE" 2>&1 &
14
+
15
+ exit 0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports["oauth-forwarder"]=r():e["oauth-forwarder"]=r()}(this,(()=>(()=>{"use strict";var e={17:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.buildOutputWriter=function(e){return r=>{e.stream.write((0,o.color)((0,s.sanitize)(r),e.color)+"\n")}};const o=t(965),s=t(799)},97:(e,r)=>{var t;Object.defineProperty(r,"__esModule",{value:!0}),r.Result=r.ResultType=void 0,function(e){e[e.success=0]="success",e[e.failure=1]="failure"}(t||(r.ResultType=t={})),r.Result={success:e=>({type:t.success,value:e}),isSuccess:e=>e.type===t.success,failure:e=>({type:t.failure,error:e}),isFailure:e=>e.type===t.failure}},100:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.parseServerInfo=function(e){const r=e.match(/^(.+):(\d+)$/);if(r){const[,e,t]=r;if(!e)return o.Result.failure(new Error("No host defined"));if(!t)return o.Result.failure(new Error("No port defined"));const s=parseInt(t,10);return isNaN(s)||s<0||s>65535?o.Result.failure(new Error(`Not a valid port: ${s}`)):o.Result.success({host:e,port:s})}return o.Result.failure(new Error("Invalid server info format, must be either a host:port or a valid socket path"))};const o=t(97)},474:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.buildBrowserHelper=function(e){return async r=>{const t=e.debugger?e.debugger:()=>{};if(!r)return t("No url argument present"),void e.onExit.failure();t(`Received url "${r}"`);try{const{redirectUrl:o}=await e.credentialForwarder(r);t(`Received redirect url ${o}`),t("Redirecting ..."),await e.redirect(o),t("Exiting on success..."),e.onExit.success()}catch(r){t(`Browser helper error "${r}"`),t("Exiting on failure..."),e.onExit.failure()}}}},611:e=>{e.exports=require("http")},689:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.EnvKey=void 0,r.EnvKey={PORT:"OAUTH2_FORWARDER_PORT",SERVER:"OAUTH2_FORWARDER_SERVER",DEBUG:"OAUTH2_FORWARDER_DEBUG"}},703:function(e,r,t){var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.buildCredentialForwarder=function(e){const r=e.debugger?e.debugger:()=>{};return t=>new Promise(((o,u)=>{const n={path:"/",method:"POST"};n.port=e.port,n.host=e.host;const i=s.default.request(n,(e=>{let t="";e.setEncoding("utf8"),e.on("data",(e=>{r(`Data chunk received: "${e}"`),t+=e})),e.on("error",(e=>{r(`Response received error: "${e}"`),u(e)})),e.on("end",(()=>{const{statusCode:s,statusMessage:n}=e;r(`Status: ${s??"No Code"}-${n??"No message"}`),200!==s&&u(`Http request failed: Status: ${s??"No Code"}-${n??"No message"}`),r(`Final output: "${t}"`);const i=JSON.parse(t);"url"in i||u("Response did not contain 'url' property"),o({redirectUrl:i.url})})),e.on("close",(()=>{r("Response closed")}))})),c={url:t},d=JSON.stringify(c);r(`Sending request body: "${d}"`),i.write(d),r("Ending request"),i.end()}))};const s=o(t(611))},799:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.sanitize=function(e){return e.replace(/((code|code_challenge)=)([^"&\n]*)/gi,((e,r)=>`${r}********`))}},965:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.color=function(e,r){return`${t[r]}${e}${o}`};const t={black:"",red:"",green:"",yellow:"",blue:"",magenta:"",cyan:"",white:""},o=""},992:function(e,r,t){var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.buildRedirect=function(e){const r=e.debugger?e.debugger:()=>{};return async e=>(r(`Making GET request to url: "${e}"`),new Promise(((t,o)=>{s.default.get(e,(e=>{200!==e.statusCode&&302!==e.statusCode?o(`Request returned unexpected status: ${e.statusCode}`):(r(`Received status ${e.statusCode}`),t())})).on("error",(e=>{r(`Received error "${JSON.stringify(e)}"`),o(e.message)}))})))};const s=o(t(611))}},r={};function t(o){var s=r[o];if(void 0!==s)return s.exports;var u=r[o]={exports:{}};return e[o].call(u.exports,u,u.exports,t),u.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0});const r=t(689),s=t(17),u=t(97),n=t(474),i=t(703),c=t(992),d=t(100),a=process.env[r.EnvKey.DEBUG],l=(0,s.buildOutputWriter)({color:"red",stream:process.stderr}),p=process.env[r.EnvKey.SERVER];p||(l(`The environmental variable ${[r.EnvKey.SERVER]} was not defined`),process.exit(1));const f=(0,d.parseServerInfo)(p);u.Result.isFailure(f)&&(l(`Invalid server info: "${f.error.message}"`),process.exit(1));const v=f.value,g=(0,i.buildCredentialForwarder)({host:v.host,port:v.port,debugger:a?(0,s.buildOutputWriter)({color:"cyan",stream:process.stderr}):void 0}),R=(0,c.buildRedirect)({debugger:a?(0,s.buildOutputWriter)({color:"yellow",stream:process.stderr}):void 0});(0,n.buildBrowserHelper)({onExit:{success:function(){process.exit(0)},failure:function(){process.exit(1)}},credentialForwarder:g,redirect:R,debugger:a?(0,s.buildOutputWriter)({color:"green",stream:process.stderr}):void 0})(process.argv[2]).catch(l)})(),o})()));
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ !function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports["oauth-forwarder"]=r():e["oauth-forwarder"]=r()}(this,(()=>(()=>{"use strict";var e={9:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.parseOauth2Url=function(e){if(!e)return o.Result.failure(new Error("Url parameter was undefined"));const r=["client_id","response_type","redirect_uri","scope","code_challenge","code_challenge_method"],t=new URL(e),n=Object.fromEntries(t.searchParams.entries());for(const e of r)if(!n[e])return o.Result.failure(new Error(`Missing required parameter: ${e}`));if(!["S256","plain"].includes(n.code_challenge_method))return o.Result.failure(new Error(`${n.code_challenge_method} is not valid for "code_challenge_method" property`));if(n.response_mode&&!["query","fragment","form_post"].includes(n.response_mode))return o.Result.failure(new Error(`${n.response_mode} is not valid for "response_mode" property`));if(n.prompt&&!["login","none","consent","select_account"].includes(n.prompt))return o.Result.failure(new Error(`${n.prompt} is not valid for "prompt" property`));const i={client_id:n.client_id,response_type:n.response_type,redirect_uri:n.redirect_uri,scope:n.scope,code_challenge:n.code_challenge,code_challenge_method:n.code_challenge_method,response_mode:n.response_mode,state:n.state,prompt:n.prompt,login_hint:n.login_hint,domain_hint:n.domain_hint,"x-client-SKU":n["x-client-SKU"],"x-client-VER":n["x-client-VER"],"x-client-OS":n["x-client-OS"],"x-client-CPU":n["x-client-CPU"],client_info:n.client_info};return o.Result.success(i)};const o=t(97)},17:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.buildOutputWriter=function(e){return r=>{e.stream.write((0,o.color)((0,n.sanitize)(r),e.color)+"\n")}};const o=t(965),n=t(799)},23:e=>{e.exports=require("util")},97:(e,r)=>{var t;Object.defineProperty(r,"__esModule",{value:!0}),r.Result=r.ResultType=void 0,function(e){e[e.success=0]="success",e[e.failure=1]="failure"}(t||(r.ResultType=t={})),r.Result={success:e=>({type:t.success,value:e}),isSuccess:e=>e.type===t.success,failure:e=>({type:t.failure,error:e}),isFailure:e=>e.type===t.failure}},152:function(e,r,t){var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0});const n=t(689),i=t(17),s=t(541),a=t(157),c=t(989),l=o(t(449)),u=process.env[n.EnvKey.DEBUG],d="127.0.0.1",p=(0,i.buildOutputWriter)({color:"cyan",stream:process.stdout}),f=(0,i.buildOutputWriter)({color:"yellow",stream:process.stdout}),m=(0,i.buildOutputWriter)({color:"white",stream:process.stdout}),g=(0,i.buildOutputWriter)({color:"red",stream:process.stderr});let h=null;const w=process.env[n.EnvKey.PORT];if(w){const e=parseInt(w);isNaN(e)||(h=e)}const y=(0,s.buildInteractiveLogin)({openBrowser:async e=>{await(0,l.default)(e)},debugger:u?(0,i.buildOutputWriter)({color:"magenta",stream:process.stdout}):void 0});(async()=>{h&&p(`Attempting to use user specified port ${h}`);const e=h??await(0,c.findAvailablePort)(d),r=(0,a.buildCredentialProxy)({host:d,port:e,interactiveLogin:y,debugger:u?(0,i.buildOutputWriter)({color:"green",stream:process.stdout}):void 0});p(`Starting TCP server listening on ${d}:${e}`),f("Run the following command in your docker container:\n"),m(` export ${n.EnvKey.SERVER}="host.docker.internal:${e}"\n`),f("\nIn addition, you need to set the BROWSER env variable to point to the client script in the docker container. If you are using the default locations, this will work:\n"),m(" export BROWSER=~/o2f/browser.sh\n");try{await r(),p("Ctrl+c to stop server.")}catch(e){g(JSON.stringify(e)),process.exit(1)}})().catch((e=>{g(JSON.stringify(e)),process.exit(1)}))},157:function(e,r,t){var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.buildCredentialProxy=function(e){return async()=>{const r=e.debugger?e.debugger:()=>{},t=n.default.createServer(((t,o)=>{r(`Request received at ${t.headers.host}`);const n=[];t.on("close",(()=>{r("Request closed.")})),t.on("error",(e=>{r(`Request received error "${e}"`)})),t.on("data",(e=>{n.push(e)})),t.on("end",(()=>{r("Request ended");const t=JSON.parse(n.join(""));let c;if(r(`Received body: "${n.join("")}"`),!("url"in t)){const e="Received body does not contain a 'url' property";return r(`Error: ${e}`),o.writeHead(400,e),void o.end()}{const e=(0,i.parseOauth2Url)(t.url);if(s.Result.isFailure(e))return r(`Error: ${e.error.message}`),o.writeHead(400,e.error.message),void o.end();c=e.value}const l=(0,a.extractPort)(c.redirect_uri);if(s.Result.isFailure(l))return r(`Error: ${l.error.message}`),o.writeHead(400,l.error.message),void o.end();const u=l.value??80;r(`Using port number: ${u}`),e.interactiveLogin(t.url,u).then((e=>{r("Interactive login completed"),r("Sending success header"),o.writeHead(200);const t={url:e};r(`Ending response with output: "${JSON.stringify(t)}"`),o.end(JSON.stringify(t))})).catch((e=>{r(`Interactive login errored: "${JSON.stringify(e)}"`),r("Sending error header"),o.writeHead(500,JSON.stringify(e)),r("Ending response"),o.end()}))}))}));return r("Starting credential proxy..."),t.listen(e.port,e.host),{close:()=>t.close()}}};const n=o(t(611)),i=t(9),s=t(97),a=t(900)},449:(e,r,t)=>{t.r(r),t.d(r,{apps:()=>L,default:()=>H,openApp:()=>F});const o=require("node:process"),n=require("node:buffer"),i=require("node:path"),s=require("node:url"),a=require("node:util"),c=require("node:child_process"),l=require("node:fs/promises"),u=require("node:os"),d=require("node:fs");let p,f;function m(){return void 0===f&&(f=(()=>{try{return d.statSync("/run/.containerenv"),!0}catch{return!1}})()||(void 0===p&&(p=function(){try{return d.statSync("/.dockerenv"),!0}catch{return!1}}()||function(){try{return d.readFileSync("/proc/self/cgroup","utf8").includes("docker")}catch{return!1}}()),p)),f}const g=()=>{if("linux"!==o.platform)return!1;if(u.release().toLowerCase().includes("microsoft"))return!m();try{return!!d.readFileSync("/proc/version","utf8").toLowerCase().includes("microsoft")&&!m()}catch{return!1}},h=o.env.__IS_WSL_TEST__?g:g();function w(e,r,t){const o=t=>Object.defineProperty(e,r,{value:t,enumerable:!0,writable:!0});return Object.defineProperty(e,r,{configurable:!0,enumerable:!0,get(){const e=t();return o(e),e},set(e){o(e)}}),e}const y=(0,a.promisify)(c.execFile),v=(0,a.promisify)(c.execFile);async function _(e){return async function(e,{humanReadableOutput:r=!0}={}){if("darwin"!==o.platform)throw new Error("macOS only");const t=r?[]:["-ss"],{stdout:n}=await v("osascript",["-e",e,t]);return n.trim()}(`tell application "Finder" to set app_path to application file id "${e}" as string\ntell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`)}const x=(0,a.promisify)(c.execFile),b={AppXq0fevzme2pys62n3e0fbqa7peapykr8v:{name:"Edge",id:"com.microsoft.edge.old"},MSEdgeDHTML:{name:"Edge",id:"com.microsoft.edge"},MSEdgeHTM:{name:"Edge",id:"com.microsoft.edge"},"IE.HTTP":{name:"Internet Explorer",id:"com.microsoft.ie"},FirefoxURL:{name:"Firefox",id:"org.mozilla.firefox"},ChromeHTML:{name:"Chrome",id:"com.google.chrome"},BraveHTML:{name:"Brave",id:"com.brave.Browser"},BraveBHTML:{name:"Brave Beta",id:"com.brave.Browser.beta"},BraveSSHTM:{name:"Brave Nightly",id:"com.brave.Browser.nightly"}};class E extends Error{}const S=(0,a.promisify)(c.execFile);const R=a.promisify(c.execFile),O=i.dirname((0,s.fileURLToPath)("file:///home/runner/work/oauth2-forwarder/oauth2-forwarder/node_modules/.pnpm/open@10.1.2/node_modules/open/index.js")),P=i.join(O,"xdg-open"),{platform:$,arch:A}=o,M=(()=>{const e="/mnt/";let r;return async function(){if(r)return r;const t="/etc/wsl.conf";let o=!1;try{await l.access(t,l.constants.F_OK),o=!0}catch{}if(!o)return e;const n=await l.readFile(t,{encoding:"utf8"}),i=/(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(n);return i?(r=i.groups.mountPoint.trim(),r=r.endsWith("/")?r:`${r}/`,r):e}})(),T=async()=>`${await M()}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`,C=async(e,r)=>{let t;for(const o of e)try{return await r(o)}catch(e){t=e}throw t},U=async e=>{if(e={wait:!1,background:!1,newInstance:!1,allowNonzeroExitCode:!1,...e},Array.isArray(e.app))return C(e.app,(r=>U({...e,app:r})));let r,{name:t,arguments:i=[]}=e.app??{};if(i=[...i],Array.isArray(t))return C(t,(r=>U({...e,app:{name:r,arguments:i}})));if("browser"===t||"browserPrivate"===t){const r={"com.google.chrome":"chrome","google-chrome.desktop":"chrome","org.mozilla.firefox":"firefox","firefox.desktop":"firefox","com.microsoft.msedge":"edge","com.microsoft.edge":"edge","com.microsoft.edgemac":"edge","microsoft-edge.desktop":"edge"},s={chrome:"--incognito",firefox:"--private-window",edge:"--inPrivate"},a=h?await async function(){const e=await T(),r=n.Buffer.from('(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice").ProgId',"utf16le").toString("base64"),{stdout:t}=await R(e,["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand",r],{encoding:"utf8"}),o=t.trim(),i={ChromeHTML:"com.google.chrome",MSEdgeHTM:"com.microsoft.edge",FirefoxURL:"org.mozilla.firefox"};return i[o]?{id:i[o]}:{}}():await async function(){if("darwin"===o.platform){const e=await async function(){if("darwin"!==o.platform)throw new Error("macOS only");const{stdout:e}=await y("defaults",["read","com.apple.LaunchServices/com.apple.launchservices.secure","LSHandlers"]),r=/LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(e);return r?.groups.id??"com.apple.Safari"}();return{name:await _(e),id:e}}if("linux"===o.platform){const{stdout:e}=await S("xdg-mime",["query","default","x-scheme-handler/http"]),r=e.trim();return{name:r.replace(/.desktop$/,"").replace("-"," ").toLowerCase().replaceAll(/(?:^|\s|-)\S/g,(e=>e.toUpperCase())),id:r}}if("win32"===o.platform)return async function(e=x){const{stdout:r}=await e("reg",["QUERY"," HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice","/v","ProgId"]),t=/ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(r);if(!t)throw new E(`Cannot find Windows browser in stdout: ${JSON.stringify(r)}`);const{id:o}=t.groups,n=b[o];if(!n)throw new E(`Unknown browser ID: ${o}`);return n}();throw new Error("Only macOS, Linux, and Windows are supported")}();if(a.id in r){const o=r[a.id];return"browserPrivate"===t&&i.push(s[o]),U({...e,app:{name:L[o],arguments:i}})}throw new Error(`${a.name} is not supported as a default browser`)}const s=[],a={};if("darwin"===$)r="open",e.wait&&s.push("--wait-apps"),e.background&&s.push("--background"),e.newInstance&&s.push("--new"),t&&s.push("-a",t);else if("win32"===$||h&&!m()&&!t){r=h?await T():`${o.env.SYSTEMROOT||o.env.windir||"C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell`,s.push("-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand"),h||(a.windowsVerbatimArguments=!0);const c=["Start"];e.wait&&c.push("-Wait"),t?(c.push(`"\`"${t}\`""`),e.target&&i.push(e.target)):e.target&&c.push(`"${e.target}"`),i.length>0&&(i=i.map((e=>`"\`"${e}\`""`)),c.push("-ArgumentList",i.join(","))),e.target=n.Buffer.from(c.join(" "),"utf16le").toString("base64")}else{if(t)r=t;else{const e=!O||"/"===O;let t=!1;try{await l.access(P,l.constants.X_OK),t=!0}catch{}r=o.versions.electron??("android"===$||e||!t)?"xdg-open":P}i.length>0&&s.push(...i),e.wait||(a.stdio="ignore",a.detached=!0)}"darwin"===$&&i.length>0&&s.push("--args",...i),e.target&&s.push(e.target);const u=c.spawn(r,s,a);return e.wait?new Promise(((r,t)=>{u.once("error",t),u.once("close",(o=>{!e.allowNonzeroExitCode&&o>0?t(new Error(`Exited with code ${o}`)):r(u)}))})):(u.unref(),u)},F=(e,r)=>{if("string"!=typeof e&&!Array.isArray(e))throw new TypeError("Expected a valid `name`");const{arguments:t=[]}=r??{};if(null!=t&&!Array.isArray(t))throw new TypeError("Expected `appArguments` as Array type");return U({...r,app:{name:e,arguments:t}})};function j(e){if("string"==typeof e||Array.isArray(e))return e;const{[A]:r}=e;if(!r)throw new Error(`${A} is not supported`);return r}function q({[$]:e},{wsl:r}){if(r&&h)return j(r);if(!e)throw new Error(`${$} is not supported`);return j(e)}const L={};w(L,"chrome",(()=>q({darwin:"google chrome",win32:"chrome",linux:["google-chrome","google-chrome-stable","chromium"]},{wsl:{ia32:"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",x64:["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe","/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]}}))),w(L,"firefox",(()=>q({darwin:"firefox",win32:"C:\\Program Files\\Mozilla Firefox\\firefox.exe",linux:"firefox"},{wsl:"/mnt/c/Program Files/Mozilla Firefox/firefox.exe"}))),w(L,"edge",(()=>q({darwin:"microsoft edge",win32:"msedge",linux:["microsoft-edge","microsoft-edge-dev"]},{wsl:"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"}))),w(L,"browser",(()=>"browser")),w(L,"browserPrivate",(()=>"browserPrivate"));const H=(e,r)=>{if("string"!=typeof e)throw new TypeError("Expected a `target`");return U({...r,target:e})}},541:function(e,r,t){var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.buildInteractiveLogin=function(e){const r=e.debugger?e.debugger:()=>{};return async(t,o)=>new Promise(((i,s)=>{let a="";const c=n.default.createServer(((e,t)=>{r(`Received a ${e.method??"undefined"} request`),e.on("error",(e=>{r(`Error: ${JSON.stringify(e)}`),r("Terminating request on error"),t.writeHead(500,JSON.stringify(e)),r("Ending response"),t.end(),r("Closing temporary redirect server on error"),c.close(),s(e)})),e.on("data",(e=>{r(`Received data chunk: ${e}`)})),e.on("close",(()=>{r("Redirect request closed")})),e.on("end",(()=>{r("Request ended"),a="http://"+e.headers.host+e.url,r(`Received request url: "${a}"`),r("Successfully terminating request"),t.writeHead(200),t.end("Authentication completed. You may close this page now."),r("Closing temporary redirect server on success"),c.close(),i(a)}))}));r(`Starting temporary redirect server on port ${o}...`),c.listen(o,"127.0.0.1"),r("Opening browser for interactive login..."),e.openBrowser(t)}))};const n=o(t(611))},611:e=>{e.exports=require("http")},689:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.EnvKey=void 0,r.EnvKey={PORT:"OAUTH2_FORWARDER_PORT",SERVER:"OAUTH2_FORWARDER_SERVER",DEBUG:"OAUTH2_FORWARDER_DEBUG"}},799:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.sanitize=function(e){return e.replace(/((code|code_challenge)=)([^"&\n]*)/gi,((e,r)=>`${r}********`))}},900:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.extractPort=function(e){const r=e.match(/^http:\/\/localhost(?::(\d{1,5}))?$/);if(!r)return o.Result.failure(new Error("Invalid URL format"));const t=r[1]?parseInt(r[1],10):void 0;return void 0===t?o.Result.success(void 0):isNaN(t)||t<0||t>65535?o.Result.failure(new Error(`Not a valid port: ${t}`)):o.Result.success(t)};const o=t(97)},965:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.color=function(e,r){return`${t[r]}${e}${o}`};const t={black:"",red:"",green:"",yellow:"",blue:"",magenta:"",cyan:"",white:""},o=""},989:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.findAvailablePort=void 0;const o=t(611),n=t(23);r.findAvailablePort=async e=>{const t=Math.floor(64512*Math.random()+1024);return new Promise((i=>{const s=(0,o.createServer)();s.listen(t,(()=>{s.close((async()=>{await(0,n.promisify)(setTimeout)(250),i(t)}))})).on("error",(()=>{i((0,r.findAvailablePort)(e))}))}))}}},r={};function t(o){var n=r[o];if(void 0!==n)return n.exports;var i=r[o]={exports:{}};return e[o].call(i.exports,i,i.exports,t),i.exports}return t.d=(e,r)=>{for(var o in r)t.o(r,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t(152)})()));
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "oauth2-forwarder",
3
+ "version": "1.1.0",
4
+ "description": "utilities for forwarding oauth2 interactive flow (e.g. container to host)",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "o2f-server": "./dist/o2f-server.js",
8
+ "o2f-client": "./dist/o2f-client.js",
9
+ "o2f-browser": "./browser-global.sh"
10
+ },
11
+ "author": "Sam Davidoff",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/sam-mfb/oauth2-forwarder.git"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "browser.sh",
20
+ "browser-global.sh"
21
+ ],
22
+ "keywords": [
23
+ "oauth2",
24
+ "docker",
25
+ "container",
26
+ "authentication",
27
+ "browser"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@eslint/js": "^9.25.1",
34
+ "@types/eslint__js": "^8.42.3",
35
+ "@types/jest": "^29.5.14",
36
+ "@types/node": "^22.15.3",
37
+ "eslint": "^9.25.1",
38
+ "jest": "^29.7.0",
39
+ "prettier": "^3.5.3",
40
+ "rimraf": "^6.0.1",
41
+ "ts-jest": "^29.3.2",
42
+ "ts-loader": "^9.5.2",
43
+ "typescript": "^5.8.3",
44
+ "typescript-eslint": "^8.31.1",
45
+ "webpack": "^5.99.7",
46
+ "webpack-cli": "^5.1.4"
47
+ },
48
+ "dependencies": {
49
+ "open": "^10.1.2"
50
+ },
51
+ "scripts": {
52
+ "build": "rimraf ./dist && webpack && node prepend-shebang.js",
53
+ "zip-release": "./release.sh",
54
+ "test": "jest --watch",
55
+ "e2e-test": "jest -c ./jest.e2e.config.js",
56
+ "lint": "eslint .",
57
+ "fix-formatting": "prettier --write --config ./prettier.config.js ./src/",
58
+ "check-formatting": "prettier --check --config ./prettier.config.js ./src/"
59
+ }
60
+ }