Skip to content

Commit

Permalink
fix: change google oauth OOB flow to server
Browse files Browse the repository at this point in the history
  • Loading branch information
Jerimo17 authored and kodiakhq[bot] committed Nov 29, 2022
1 parent d679685 commit b3861db
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
.nyc_output
.nyc_output
.idea
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ CLIENT_ID - OAuth Client ID for authenticating from a desktop app
CLIENT_SECRET - OAuth Client secret for authenticating from a desktop app
```

**These are optional environment variables:**

```
GOOGLE_REDIRECT_URI - local server uri, which uses google OAuth for redirecting (default 'http://localhost:3000')
OAUTH_SERVER_PORT - your local server port (default '3000')
```

`package.json`

```json
Expand All @@ -150,7 +157,7 @@ CLIENT_SECRET - OAuth Client secret for authenticating from a desktop app
}
```

Now run `generate:token` a browser will open displaying an authorization code (starting with `4/`) and the CLI will ask you to input the authorization code. After that you will be provided with a `refresh_token` (starting with `1/`) that has long validity and can be used for local development.
Now run `generate:token`. This script will start local server (on default port 3000) and the server will automatically open your browser with Google's OAuth web page for your application. After clicking on the button, Google will redirect you back to your GOOGLE_REDIRECT_URI (which defaults to http://localhost:3000) and you will be provided with a `refresh_token` (starting with `1/`) that has long validity and can be used for local development.

# Contributing

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"jsonwebtoken": "^8.5.1",
"mamushi": "^2.0.0",
"node-fetch": "^2.6.0",
"open": "^8.4.0"
"open": "^8.4.0",
"url": "^0.11.0"
},
"devDependencies": {
"@semantic-release/commit-analyzer": "9.0.2",
Expand Down
29 changes: 8 additions & 21 deletions src/scripts/getRefreshToken.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import test from "ava";
import { openBrowser, getRefreshToken } from "./getRefreshToken";
import { openBrowser, getRefreshToken, isASCII } from "./getRefreshToken";
import { Response } from "node-fetch";

test("isASCII", (t) => {
t.truthy(isASCII("nice456452413%"));
t.falsy(isASCII("£"));
});

test("opens a browser window", (t) => {
let nOpenBrowserCalls = 0;
const testUrl = "http://test.com/";
Expand All @@ -16,48 +21,30 @@ test("opens a browser window", (t) => {
});

test("getRefreshToken", async (t) => {
let nOpenBrowserCalls = 0;
let nFetchCalls = 0;
let nGetInputCalls = 0;
const testUrl =
"https://accounts.google.com/o/oauth2/v2/auth?client_id=test_id&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob";

const iapOptions = {
clientId: "test_id",
clientSecret: "test",
iapClientId: "not_used",
};

function mockOpenBrowser(url: string): void {
nOpenBrowserCalls += 1;
t.is(url, testUrl);
}

const mockFetch = (response: any) => async (): Promise<Response> => {
nFetchCalls += 1;
return new Response(JSON.stringify(response));
};

async function mockUserInput(): Promise<string> {
nGetInputCalls += 1;
return new Promise((resolve) => {
resolve("test_client_id");
});
}

const response = {
refresh_token: "some_refresh_token",
};

const refreshToken = await getRefreshToken(
mockOpenBrowser,
mockUserInput,
"?code=4/0Afge",
mockFetch(response),
iapOptions,
"localhost",
);

t.is(nOpenBrowserCalls, 1);
t.is(nFetchCalls, 1);
t.is(nGetInputCalls, 1);
t.is(refreshToken, "some_refresh_token");
});
73 changes: 42 additions & 31 deletions src/scripts/getRefreshToken.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,54 @@
import fetch from "node-fetch";
import { DesktopIAPOptions, Fetcher } from "../types";
import url from "url";

export function openBrowser(browser: (url: string) => void, url: string): void {
browser(url);
}
export const isASCII = (value: string): boolean => /^[\x00-\x7F]+$/.test(value);

export async function getRefreshToken(
browser: (url: string) => void,
inputHandler: (question: string) => Promise<string>,
redirectUrl: string,
fetcher: Fetcher = fetch,
options: DesktopIAPOptions,
redirectUri: string,
): Promise<string> {
const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${options.clientId}&response_type=code&scope=openid%20email&access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob`;

console.log("Open this URL if your browser didn't open automatically\n", url);

openBrowser(browser, url);

// await input from user
const loginToken = await inputHandler(
"Please input the token you received in the browser",
);

const oauthTokenBaseUrl = "https://www.googleapis.com/oauth2/v4/token";

const body = {
code: loginToken,
client_id: options.clientId,
client_secret: options.clientSecret,
redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
grant_type: "authorization_code",
};

const request = await fetcher(oauthTokenBaseUrl, {
body: JSON.stringify(body),
method: "POST",
});

const response = await request.json();

return String(response["refresh_token"]);
const { code, error } = url.parse(redirectUrl, true).query;
if (error) {
// An error response e.g. error=access_denied
console.log("Error:" + error);
return "";
} else {
if (!code || code === "") {
console.log("Authorization code is empty");
return "";
}
if (Array.isArray(code)) {
console.log("There is more then one authorization code in redirect url.");
return "";
}
if (!isASCII(code)) {
console.log("Code contains not allowed characters: " + code);
return "";
}

const oauthTokenBaseUrl = "https://www.googleapis.com/oauth2/v4/token";

const body = {
code: code,
client_id: options.clientId,
client_secret: options.clientSecret,
redirect_uri: redirectUri,
grant_type: "authorization_code",
};

const request = await fetcher(oauthTokenBaseUrl, {
body: JSON.stringify(body),
method: "POST",
});

const response = await request.json();

return String(response["refresh_token"]);
}
}
61 changes: 48 additions & 13 deletions src/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,62 @@ import { GetString } from "mamushi";
import open from "open";
import fetch from "node-fetch";
import { getRefreshToken } from "./getRefreshToken";
import { handleUserInput } from "./inputHandler";
import http from "http";
import url from "url";

async function start(): Promise<void> {
const options = {
clientId: GetString("CLIENT_ID"),
clientSecret: GetString("CLIENT_SECRET"),
iapClientId: GetString("IAP_CLIENT_ID"),
};
const refreshToken = await getRefreshToken(
open,
handleUserInput,
fetch,
options,
const redirectUri =
GetString("GOOGLE_REDIRECT_URI") || "http://localhost:3000";
const serverPort = Number(GetString("OAUTH_SERVER_PORT")) || 3000;
const authorizationUrl = url.resolve(
"https://accounts.google.com/o/oauth2/v2/auth",
url.format({
query: {
client_id: options.clientId,
response_type: "code",
scope: "openid email",
access_type: "offline",
include_granted_scopes: true,
redirect_uri: redirectUri,
},
}),
);

console.log(`
Refresh token generated. Please save it to your .env file:
\`\`\`
IAP_REFRESH_TOKEN=${refreshToken}
\`\`\`
`);
http
.createServer(async function (req, res) {
// Receive the callback from Google's OAuth 2.0 server.
if (req?.url?.startsWith("/?code")) {
// Handle the OAuth 2.0 server response
const refreshToken = await getRefreshToken(
req.url,
fetch,
options,
redirectUri,
);

const refreshTokenInfo = `\nRefresh token generated. Please save it to your .env file:\nIAP_REFRESH_TOKEN=${refreshToken}`;
res.end(refreshTokenInfo, () => {
console.log(refreshTokenInfo);

// Ending server
process.kill(process.pid, "SIGTERM");
});
}
})
.listen(serverPort, () => {
console.log(`🚀 Server ready at ${serverPort}`);
// open the browser to authorize url to start the workflow
console.log(
"Open this URL if your browser didn't open automatically\n",
authorizationUrl,
);
open(authorizationUrl, { wait: false }).then((cp) => cp.unref());
});
}

start();
start().catch(console.error);
15 changes: 0 additions & 15 deletions src/scripts/inputHandler.ts

This file was deleted.

18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6852,6 +6852,11 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"

[email protected]:
version "1.3.2"
resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==

punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
Expand All @@ -6874,6 +6879,11 @@ [email protected]:
dependencies:
side-channel "^1.0.4"

[email protected]:
version "0.2.0"
resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==

quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
Expand Down Expand Up @@ -8240,6 +8250,14 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"

url@^0.11.0:
version "0.11.0"
resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==
dependencies:
punycode "1.3.2"
querystring "0.2.0"

util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit b3861db

Please sign in to comment.