Skip to content

Commit 40e954a

Browse files
lollipop-onlclaude
andauthored
feat: configurable OAuth callback port & remove client flags (#71)
* feat(backlog-utils): accept custom port in startCallbackServer * test(backlog-utils): use random ports in oauth-callback tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(cli): remove --client-id/--client-secret flags, add BACKLOG_OAUTH_PORT support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(cli): update login tests for env-only OAuth config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update OAuth docs for env-only config and BACKLOG_OAUTH_PORT Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: use callout for BACKLOG_OAUTH_PORT tip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(cli): require OAuth credentials via env vars instead of interactive prompt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update OAuth docs to reflect required env vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: import AddressInfo from node:net instead of node:http Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix formatting in login tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 117bff6 commit 40e954a

6 files changed

Lines changed: 102 additions & 136 deletions

File tree

apps/cli/src/commands/auth/login.test.ts

Lines changed: 38 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { promptRequired } from "@repo/cli-utils";
33
import { updateConfig } from "@repo/config";
44
import { Backlog, OAuth2 } from "backlog-js";
55
import consola from "consola";
6-
import { describe, expect, it, vi } from "vitest";
6+
import { afterEach, describe, expect, it, vi } from "vitest";
77
import { parseCommand } from "@repo/test-utils";
88

99
const mockGetMyself = vi.fn();
@@ -116,6 +116,9 @@ describe("auth login", () => {
116116

117117
describe("oauth", () => {
118118
const setupOAuthMocks = () => {
119+
process.env.BACKLOG_OAUTH_CLIENT_ID = "my-client-id";
120+
process.env.BACKLOG_OAUTH_CLIENT_SECRET = "my-client-secret";
121+
119122
mockGetMyself.mockResolvedValue({ name: "OAuth User", userId: "oauthuser" });
120123
vi.mocked(updateConfig).mockImplementation((updater) =>
121124
updater({ spaces: [], defaultSpace: undefined, aliases: {} }),
@@ -138,17 +141,24 @@ describe("auth login", () => {
138141
return { mockStop, mockWaitForCallback };
139142
};
140143

144+
afterEach(() => {
145+
delete process.env.BACKLOG_OAUTH_CLIENT_ID;
146+
delete process.env.BACKLOG_OAUTH_CLIENT_SECRET;
147+
});
148+
149+
it("throws error when OAuth env vars are missing", async () => {
150+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
151+
152+
await expect(parseCommand(() => import("./login"), ["--method", "oauth"])).rejects.toThrow(
153+
"BACKLOG_OAUTH_CLIENT_ID and BACKLOG_OAUTH_CLIENT_SECRET must be set as environment variables.",
154+
);
155+
});
156+
141157
it("authenticates new space via OAuth flow", async () => {
142158
setupOAuthMocks();
143-
vi.mocked(promptRequired)
144-
.mockResolvedValueOnce("example.backlog.com")
145-
.mockResolvedValueOnce("my-client-id")
146-
.mockResolvedValueOnce("my-client-secret");
159+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
147160

148-
await parseCommand(
149-
() => import("./login"),
150-
["--method", "oauth", "--client-id", "my-client-id", "--client-secret", "my-client-secret"],
151-
);
161+
await parseCommand(() => import("./login"), ["--method", "oauth"]);
152162

153163
expect(startCallbackServer).toHaveBeenCalled();
154164
expect(OAuth2).toHaveBeenCalledWith({
@@ -191,6 +201,8 @@ describe("auth login", () => {
191201
});
192202

193203
it("throws error when error occurs during callback", async () => {
204+
process.env.BACKLOG_OAUTH_CLIENT_ID = "my-client-id";
205+
process.env.BACKLOG_OAUTH_CLIENT_SECRET = "my-client-secret";
194206
const mockStop = vi.fn();
195207
vi.mocked(startCallbackServer).mockReturnValue({
196208
port: 5033,
@@ -199,71 +211,32 @@ describe("auth login", () => {
199211
.mockRejectedValue(new Error("OAuth callback timed out after 5 minutes")),
200212
stop: mockStop,
201213
});
202-
vi.mocked(promptRequired)
203-
.mockResolvedValueOnce("example.backlog.com")
204-
.mockResolvedValueOnce("my-client-id")
205-
.mockResolvedValueOnce("my-client-secret");
206-
207-
await expect(
208-
parseCommand(
209-
() => import("./login"),
210-
[
211-
"--method",
212-
"oauth",
213-
"--client-id",
214-
"my-client-id",
215-
"--client-secret",
216-
"my-client-secret",
217-
],
218-
),
219-
).rejects.toThrow("OAuth authorization failed: OAuth callback timed out after 5 minutes");
214+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
215+
216+
await expect(parseCommand(() => import("./login"), ["--method", "oauth"])).rejects.toThrow(
217+
"OAuth authorization failed: OAuth callback timed out after 5 minutes",
218+
);
220219
expect(mockStop).toHaveBeenCalled();
221220
});
222221

223222
it("throws error when token exchange fails", async () => {
224223
setupOAuthMocks();
225224
vi.mocked(exchangeAuthorizationCode).mockRejectedValue(new Error("invalid_grant"));
226-
vi.mocked(promptRequired)
227-
.mockResolvedValueOnce("example.backlog.com")
228-
.mockResolvedValueOnce("my-client-id")
229-
.mockResolvedValueOnce("my-client-secret");
230-
231-
await expect(
232-
parseCommand(
233-
() => import("./login"),
234-
[
235-
"--method",
236-
"oauth",
237-
"--client-id",
238-
"my-client-id",
239-
"--client-secret",
240-
"my-client-secret",
241-
],
242-
),
243-
).rejects.toThrow("Failed to exchange authorization code for tokens.");
225+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
226+
227+
await expect(parseCommand(() => import("./login"), ["--method", "oauth"])).rejects.toThrow(
228+
"Failed to exchange authorization code for tokens.",
229+
);
244230
});
245231

246232
it("throws error when token verification fails", async () => {
247233
setupOAuthMocks();
248234
mockGetMyself.mockRejectedValue(new Error("Unauthorized"));
249-
vi.mocked(promptRequired)
250-
.mockResolvedValueOnce("example.backlog.com")
251-
.mockResolvedValueOnce("my-client-id")
252-
.mockResolvedValueOnce("my-client-secret");
253-
254-
await expect(
255-
parseCommand(
256-
() => import("./login"),
257-
[
258-
"--method",
259-
"oauth",
260-
"--client-id",
261-
"my-client-id",
262-
"--client-secret",
263-
"my-client-secret",
264-
],
265-
),
266-
).rejects.toThrow("Authentication verification failed.");
235+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
236+
237+
await expect(parseCommand(() => import("./login"), ["--method", "oauth"])).rejects.toThrow(
238+
"Authentication verification failed.",
239+
);
267240
});
268241

269242
it("updates OAuth credentials for existing space", async () => {
@@ -286,15 +259,9 @@ describe("auth login", () => {
286259
aliases: {},
287260
}),
288261
);
289-
vi.mocked(promptRequired)
290-
.mockResolvedValueOnce("example.backlog.com")
291-
.mockResolvedValueOnce("my-client-id")
292-
.mockResolvedValueOnce("my-client-secret");
262+
vi.mocked(promptRequired).mockResolvedValueOnce("example.backlog.com");
293263

294-
await parseCommand(
295-
() => import("./login"),
296-
["--method", "oauth", "--client-id", "my-client-id", "--client-secret", "my-client-secret"],
297-
);
264+
await parseCommand(() => import("./login"), ["--method", "oauth"]);
298265

299266
const result = vi.mocked(updateConfig).mock.results[0]?.value;
300267
expect(result.spaces).toEqual([

apps/cli/src/commands/auth/login.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,11 @@ Use \`--method oauth\` for OAuth authentication via the browser.`,
1515
)
1616
.option("-m, --method <method>", "The authentication method to use", "api-key")
1717
.option("--with-token", "Read token from standard input")
18-
.option("--client-id <id>", "The OAuth Client ID to use when authenticating with Backlog")
19-
.option(
20-
"--client-secret <secret>",
21-
"The OAuth Client Secret to use when authenticating with Backlog",
22-
)
2318
.envVars([
2419
["BACKLOG_SPACE", "Default space hostname"],
2520
["BACKLOG_OAUTH_CLIENT_ID", "OAuth Client ID"],
2621
["BACKLOG_OAUTH_CLIENT_SECRET", "OAuth Client Secret"],
22+
["BACKLOG_OAUTH_PORT", "OAuth callback port (default: 5033)"],
2723
])
2824
.examples([
2925
{ description: "Start interactive setup", command: "bee auth login" },
@@ -44,7 +40,7 @@ Use \`--method oauth\` for OAuth authentication via the browser.`,
4440
placeholder: "xxx.backlog.com",
4541
});
4642

47-
await (method === "api-key" ? loginWithApiKey(hostname, opts) : loginWithOAuth(hostname, opts));
43+
await (method === "api-key" ? loginWithApiKey(hostname, opts) : loginWithOAuth(hostname));
4844
});
4945

5046
const loginWithApiKey = async (hostname: string, opts: { withToken?: boolean }): Promise<void> => {
@@ -69,21 +65,20 @@ const loginWithApiKey = async (hostname: string, opts: { withToken?: boolean }):
6965
consola.success(`Logged in to ${hostname} as ${user.name} (${user.userId})`);
7066
};
7167

72-
const loginWithOAuth = async (
73-
hostname: string,
74-
opts: { clientId?: string; clientSecret?: string },
75-
): Promise<void> => {
76-
const clientId = await promptRequired(
77-
"OAuth Client ID:",
78-
opts.clientId ?? process.env.BACKLOG_OAUTH_CLIENT_ID,
79-
);
80-
81-
const clientSecret = await promptRequired(
82-
"OAuth Client Secret:",
83-
opts.clientSecret ?? process.env.BACKLOG_OAUTH_CLIENT_SECRET,
84-
);
85-
86-
const callbackServer = startCallbackServer();
68+
const loginWithOAuth = async (hostname: string): Promise<void> => {
69+
const clientId = process.env.BACKLOG_OAUTH_CLIENT_ID;
70+
const clientSecret = process.env.BACKLOG_OAUTH_CLIENT_SECRET;
71+
if (!clientId || !clientSecret) {
72+
throw new UserError(
73+
"BACKLOG_OAUTH_CLIENT_ID and BACKLOG_OAUTH_CLIENT_SECRET must be set as environment variables.",
74+
);
75+
}
76+
77+
const port = process.env.BACKLOG_OAUTH_PORT ? Number(process.env.BACKLOG_OAUTH_PORT) : undefined;
78+
if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65_535)) {
79+
throw new UserError("BACKLOG_OAUTH_PORT must be an integer between 0 and 65535.");
80+
}
81+
const callbackServer = startCallbackServer(port);
8782
const redirectUri = `http://localhost:${callbackServer.port}/callback`;
8883
const state = crypto.randomUUID();
8984

apps/docs/src/content/docs/guides/authentication.mdx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: |-
44
bee CLI の認証方法。Backlog への API キー・OAuth 認証の設定手順、使い分け、複数スペースの切り替えに対応。
55
---
66

7-
import { Tabs, TabItem, Card, LinkCard, CardGrid } from "@astrojs/starlight/components";
7+
import { Card, LinkCard, CardGrid } from "@astrojs/starlight/components";
88

99
bee は 2 つの認証方式をサポートしています。
1010

@@ -60,6 +60,10 @@ OAuth を使うと、API キーを直接扱わずに認証できます。
6060
- **リダイレクト URI**: `http://localhost:5033/callback`
6161
5. 登録後に表示される **Client ID****Client Secret** をメモします
6262

63+
:::tip
64+
ポート `5033` が他のアプリケーションと競合する場合は、環境変数 `BACKLOG_OAUTH_PORT` で変更できます。リダイレクト URI も合わせて変更してください。
65+
:::
66+
6367
### ログイン
6468

6569
```sh
@@ -68,22 +72,13 @@ bee auth login --method oauth
6872

6973
ブラウザが開き、Backlog の認証画面が表示されます。承認するとトークンが自動的に取得されます。
7074

71-
OAuth クライアント ID とシークレットは、フラグまたは環境変数で指定できます。
72-
73-
<Tabs>
74-
<TabItem label="フラグ">
75-
```sh
76-
bee auth login --method oauth --client-id YOUR_ID --client-secret YOUR_SECRET
77-
```
78-
</TabItem>
79-
<TabItem label="環境変数">
80-
```sh
81-
export BACKLOG_OAUTH_CLIENT_ID=YOUR_ID
82-
export BACKLOG_OAUTH_CLIENT_SECRET=YOUR_SECRET
83-
bee auth login --method oauth
84-
```
85-
</TabItem>
86-
</Tabs>
75+
事前に環境変数 `BACKLOG_OAUTH_CLIENT_ID``BACKLOG_OAUTH_CLIENT_SECRET` を設定してください。
76+
77+
```sh
78+
export BACKLOG_OAUTH_CLIENT_ID=YOUR_ID
79+
export BACKLOG_OAUTH_CLIENT_SECRET=YOUR_SECRET
80+
bee auth login --method oauth
81+
```
8782

8883
## どちらを選ぶべきか
8984

apps/docs/src/content/docs/guides/environment-variables.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ description: |-
2525
: API キーによる認証に使用します。`BACKLOG_SPACE` と併用すると、`bee auth login` を実行しなくても認証済みの状態で bee を利用できます。CI/CD 環境で特に便利です。
2626

2727
`BACKLOG_OAUTH_CLIENT_ID`
28-
: OAuth 認証で使用するクライアント ID。`bee auth login --method oauth` 時に `--client-id` フラグの代わりに使用できます
28+
: OAuth 認証で使用するクライアント ID。`bee auth login --method oauth` で必須です
2929

3030
`BACKLOG_OAUTH_CLIENT_SECRET`
31-
: OAuth 認証で使用するクライアントシークレット。`bee auth login --method oauth` 時に `--client-secret` フラグの代わりに使用できます。
31+
: OAuth 認証で使用するクライアントシークレット。`bee auth login --method oauth` で必須です。
32+
33+
`BACKLOG_OAUTH_PORT`
34+
: OAuth 認証で使用するコールバックサーバーのポート番号。デフォルトは `5033`。別のアプリケーションとポートが競合する場合に変更できます。
3235

3336
## よく使うパターン
3437

0 commit comments

Comments
 (0)