Skip to content

Commit 3011b28

Browse files
authored
feat: improve oauth login inputs and callback UX (#31)
1 parent a5fd7bf commit 3011b28

File tree

4 files changed

+421
-120
lines changed

4 files changed

+421
-120
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,26 @@ npm link
2626

2727
```sh
2828
ol auth login # opens browser for OAuth authorization
29+
ol auth login --base-url <your-outline-url> # skip base URL prompt for this login
30+
ol auth login --client-id <your-client-id> # skip prompt for this login
31+
ol auth login --callback-port 54969 # override local callback port
2932
ol auth status # show current auth state
3033
ol auth logout # clear saved credentials
3134
```
3235

3336
**Setup:**
3437

3538
1. Create a public OAuth app in Outline (Settings → Applications)
36-
2. Set the redirect URI to `http://localhost` (any port is fine)
39+
2. Set the redirect URI to `http://localhost:54969/callback`
3740
3. Run `ol auth login` and enter your OAuth client ID when prompted
41+
(or pass it directly with `--client-id <your-client-id>`)
42+
4. If needed, pass `--base-url <your-outline-url>` or set `OUTLINE_URL`
43+
(for self-hosted instances or non-default URLs)
44+
5. If needed, pass `--callback-port <port>` or set `OUTLINE_OAUTH_CALLBACK_PORT`
45+
and register `http://localhost:<port>/callback` in your OAuth app
3846

39-
The client ID is saved for future logins. You can also set `OUTLINE_OAUTH_CLIENT_ID` env var.
47+
The client ID is saved for future logins. You can also set `OUTLINE_OAUTH_CLIENT_ID`
48+
for your local environment.
4049

4150
### Manual token login
4251

@@ -54,7 +63,10 @@ Token resolution: `OUTLINE_API_TOKEN` env var → `~/.config/outline-cli/config.
5463

5564
Base URL resolution: `OUTLINE_URL` env var → config file → `https://app.getoutline.com`.
5665

57-
Self-hosted instances: provide your instance URL during `ol auth login` or set `OUTLINE_URL`.
66+
Callback port resolution for OAuth login:
67+
`--callback-port``OUTLINE_OAUTH_CALLBACK_PORT``54969`.
68+
69+
Self-hosted instances: pass `--base-url` or set `OUTLINE_URL` (you can still provide it interactively).
5870

5971
## Commands
6072

src/__tests__/oauth-server.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from "vitest";
2+
import { startOAuthCallbackServer } from "../lib/oauth-server.js";
3+
4+
describe("oauth callback server", () => {
5+
it("returns success page and resolves authorization code", async () => {
6+
const callbackServer = await startOAuthCallbackServer({
7+
state: "expected-state",
8+
timeoutMs: 10_000,
9+
port: 0,
10+
});
11+
12+
const response = await fetch(
13+
`${callbackServer.redirectUri}?code=test-code&state=expected-state`,
14+
);
15+
const html = await response.text();
16+
17+
expect(response.status).toBe(200);
18+
expect(html).toContain("Login complete");
19+
await expect(callbackServer.waitForCode).resolves.toBe("test-code");
20+
});
21+
22+
it("returns error page and rejects on state mismatch", async () => {
23+
const callbackServer = await startOAuthCallbackServer({
24+
state: "expected-state",
25+
timeoutMs: 10_000,
26+
port: 0,
27+
});
28+
const rejection = callbackServer.waitForCode.then(
29+
() => new Error("Expected OAuth state mismatch."),
30+
(error) => error as Error,
31+
);
32+
33+
const response = await fetch(
34+
`${callbackServer.redirectUri}?code=test-code&state=wrong-state`,
35+
);
36+
const html = await response.text();
37+
38+
expect(response.status).toBe(400);
39+
expect(html).toContain("Authentication failed");
40+
const error = await rejection;
41+
expect(error.message).toBe("OAuth state mismatch.");
42+
});
43+
44+
it("returns error page and rejects when OAuth provider sends an error", async () => {
45+
const callbackServer = await startOAuthCallbackServer({
46+
state: "expected-state",
47+
timeoutMs: 10_000,
48+
port: 0,
49+
});
50+
const rejection = callbackServer.waitForCode.then(
51+
() => new Error("Expected OAuth provider error."),
52+
(error) => error as Error,
53+
);
54+
55+
const response = await fetch(
56+
`${callbackServer.redirectUri}?error=access_denied&error_description=User%20denied`,
57+
);
58+
const html = await response.text();
59+
60+
expect(response.status).toBe(400);
61+
expect(html).toContain("Authentication failed");
62+
const error = await rejection;
63+
expect(error.message).toBe("OAuth authorization denied: User denied");
64+
});
65+
});

0 commit comments

Comments
 (0)