Repository navigation
feat(cli): cross-device login via OAuth code + PKCE #3965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
deee0a1
feat(cli): cross-device login via OAuth code + PKCE
saddlepaddle 20dfa6c
chore(cli): drop seed-cli-oauth-client from runtime instrumentation
saddlepaddle d06e505
chore(cli): address bot-review nits on cross-device login
saddlepaddle 7afd198
chore: drop unused allowedDevOrigins from web next.config
saddlepaddle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/CliAuthCodeDisplay.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| "use client"; | ||
|
|
||
| import { Button } from "@superset/ui/button"; | ||
| import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { LuClipboard, LuClipboardCheck } from "react-icons/lu"; | ||
|
|
||
| interface CliAuthCodeDisplayProps { | ||
| code: string; | ||
| state: string; | ||
| } | ||
|
|
||
| async function copyToClipboard(value: string): Promise<boolean> { | ||
| if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { | ||
| try { | ||
| await navigator.clipboard.writeText(value); | ||
| return true; | ||
| } catch { | ||
| // fall through | ||
| } | ||
| } | ||
| if (typeof document === "undefined") return false; | ||
| try { | ||
| const textarea = document.createElement("textarea"); | ||
| textarea.value = value; | ||
| textarea.setAttribute("readonly", ""); | ||
| textarea.style.position = "fixed"; | ||
| textarea.style.opacity = "0"; | ||
| document.body.appendChild(textarea); | ||
| textarea.select(); | ||
| const ok = document.execCommand("copy"); | ||
| document.body.removeChild(textarea); | ||
| return ok; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| function useCopiedFlag() { | ||
| const [copied, setCopied] = useState(false); | ||
| const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (timerRef.current) clearTimeout(timerRef.current); | ||
| }; | ||
| }, []); | ||
|
|
||
| const flash = () => { | ||
| setCopied(true); | ||
| if (timerRef.current) clearTimeout(timerRef.current); | ||
| timerRef.current = setTimeout(() => setCopied(false), 2000); | ||
| }; | ||
| return [copied, flash] as const; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| export function CliAuthCodeDisplay({ code, state }: CliAuthCodeDisplayProps) { | ||
| const value = `${code}#${state}`; | ||
| const [boxCopied, flashBox] = useCopiedFlag(); | ||
| const [buttonCopied, flashButton] = useCopiedFlag(); | ||
|
|
||
| const handleCopy = async (flash: () => void) => { | ||
| const ok = await copyToClipboard(value); | ||
| if (ok) flash(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="mx-auto flex w-full max-w-2xl flex-col items-center space-y-6 px-6 text-center"> | ||
| <h1 className="text-3xl font-semibold tracking-tight"> | ||
| Authentication Code | ||
| </h1> | ||
| <p className="text-muted-foreground">Paste this into Superset CLI:</p> | ||
|
|
||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <button | ||
| type="button" | ||
| onClick={() => handleCopy(flashBox)} | ||
| className="bg-muted/50 hover:bg-muted/70 focus-visible:ring-ring/50 w-full cursor-pointer overflow-x-auto rounded-lg border px-6 py-4 text-left transition-colors focus-visible:ring-[3px] focus-visible:outline-none" | ||
| > | ||
| <code className="font-mono text-sm break-all select-all"> | ||
| {value} | ||
| </code> | ||
| </button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| {boxCopied ? "Copied!" : "Click to copy"} | ||
| </TooltipContent> | ||
| </Tooltip> | ||
|
|
||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button onClick={() => handleCopy(flashButton)}> | ||
| {buttonCopied ? ( | ||
| <> | ||
| <LuClipboardCheck /> Copied! | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <LuClipboard /> Copy code | ||
| </> | ||
| )} | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| {buttonCopied ? "Copied!" : "Copy to clipboard"} | ||
| </TooltipContent> | ||
| </Tooltip> | ||
|
|
||
| <p className="text-muted-foreground text-xs"> | ||
| You can close this tab once the code is pasted. | ||
| </p> | ||
| </div> | ||
| ); | ||
| } | ||
1 change: 1 addition & 0 deletions
1
apps/web/src/app/cli/auth/code/components/CliAuthCodeDisplay/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { CliAuthCodeDisplay } from "./CliAuthCodeDisplay"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import Image from "next/image"; | ||
|
|
||
| import { env } from "@/env"; | ||
| import { CliAuthCodeDisplay } from "./components/CliAuthCodeDisplay"; | ||
|
|
||
| interface CliAuthCodePageProps { | ||
| searchParams: Promise<Record<string, string>>; | ||
| } | ||
|
|
||
| export default async function CliAuthCodePage({ | ||
| searchParams, | ||
| }: CliAuthCodePageProps) { | ||
| const params = await searchParams; | ||
| const code = params.code; | ||
| const state = params.state; | ||
| const oauthError = params.error; | ||
|
|
||
| return ( | ||
| <div className="relative flex min-h-screen flex-col"> | ||
| <header className="container mx-auto px-6 py-6"> | ||
| <a href={env.NEXT_PUBLIC_MARKETING_URL}> | ||
| <Image | ||
| src="/title.svg" | ||
| alt="Superset" | ||
| width={140} | ||
| height={24} | ||
| priority | ||
| /> | ||
| </a> | ||
| </header> | ||
| <main className="flex flex-1 items-center justify-center"> | ||
| {oauthError ? ( | ||
| <div className="mx-auto flex w-full max-w-md flex-col items-center space-y-3 text-center"> | ||
| <h1 className="text-2xl font-semibold tracking-tight text-destructive"> | ||
| Authorization failed | ||
| </h1> | ||
| <p className="text-muted-foreground text-sm"> | ||
| {params.error_description ?? oauthError} | ||
| </p> | ||
| </div> | ||
| ) : code && state ? ( | ||
| <CliAuthCodeDisplay code={code} state={state} /> | ||
| ) : ( | ||
| <p className="text-muted-foreground"> | ||
| Missing authorization code. Re-run <code>superset auth login</code>. | ||
| </p> | ||
| )} | ||
| </main> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
organizationIdclaim may never be present — all org-scoped tRPC calls will seenullBetter Auth's oauth-provider issues standard JWT claims (
sub,iat,exp,iss,aud). The customorganizationIdclaim is not a standard claim and is not emitted unless the oauth-provider plugin is explicitly configured to include it. As a resultactiveOrganizationIdwill almost always benullfor CLI bearer sessions, and any tRPC procedure that reads the active org from the session context will silently behave as if no org is selected — even after the user chose one during login.The
config.organizationIdwritten incommand.tsis never forwarded back to the API in the JWT; you'd need either (a) a custom JWT "access-token claims" hook in your Better Auth config that embedsorganizationId, or (b) the CLI passing the org explicitly on each request (e.g. a tRPC header), or (c) a separate token exchange step. Without one of these,superset auth whoamiand every org-scoped command will return wrong/empty data.Prompt To Fix With AI