Skip to content

Commit a5f2c4e

Browse files
authored
Merge pull request #274 from google/oidc_dev
node: add OAuth device flow for headless enrollment with browser-fail…
2 parents 2bce37d + 230a21a commit a5f2c4e

8 files changed

Lines changed: 889 additions & 74 deletions

File tree

agents/skills/sam-mesh/SKILL.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@ approve it before running anything.
3434
log file, and how to stop it. It is idempotent, so run it again whenever you
3535
need to confirm a node is up.
3636
3. If step 2 reports that the node is not enrolled, run the command it prints,
37-
`sam-node join --headless <control-plane-url>`. That prints a URL and a
38-
code: show both to the user and wait while they complete the login, then
39-
repeat step 2. Enrollment is a one-time step per machine.
37+
`sam-node join --headless <control-plane-url>`. In headless mode SAM now
38+
prefers OAuth device flow automatically when the OIDC provider supports it,
39+
so no pasted callback code is required: it prints a verification URL/code
40+
and polls until login completes. If the provider does not expose a device
41+
endpoint, SAM falls back to OOB code-paste flow; show the URL/code to the
42+
user, wait for completion, then repeat step 2. For deterministic automation,
43+
force the flow with `--auth-mode device` (also `oob`, `browser`, or the
44+
default `auto`); `--auth-mode device` fails fast if the provider has no
45+
device endpoint. Enrollment is a one-time step per machine.
4046
4. Read the node API token from the file named in step 2, then register the MCP
4147
endpoint `http://127.0.0.1:8080/mcp` with the header
4248
`X-Sam-Authentication: Bearer <token>`. Claude Code:

cmd/sam-box/main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ var (
7171
audienceFlag string
7272
dataDirFlag string
7373
headlessFlag bool
74+
authModeFlag string
7475
offlineAccessFlag bool
7576
logLevelFlag string
7677
keyGracePeriodFlag time.Duration
@@ -424,12 +425,21 @@ func main() {
424425
fmt.Printf("Client ID discovered: %s\n", controlPlaneInfo.ClientId)
425426

426427
logger.Info("Discovering OIDC endpoints...")
427-
tokenURL, authURL, err := dummyNode.DiscoverEndpoints(ctx, controlPlaneInfo.OidcIssuer)
428+
endpoints, err := dummyNode.DiscoverEndpointsWithDevice(ctx, controlPlaneInfo.OidcIssuer)
428429
if err != nil {
429430
logger.Fatalf("Failed to discover OIDC endpoints: %v", err)
430431
}
432+
deviceAuthURL := endpoints.DeviceAuthURL
433+
if deviceAuthURLFlag != "" {
434+
deviceAuthURL = deviceAuthURLFlag
435+
}
436+
437+
mode, err := node.ParseAuthMode(authModeFlag)
438+
if err != nil {
439+
logger.Fatalf("Invalid --auth-mode: %v", err)
440+
}
431441

432-
jwtStr, err = dummyNode.InteractiveLogin(ctx, authURL, tokenURL, controlPlaneInfo.ClientId, controlPlaneInfo.Audience, offlineAccessFlag, headlessFlag)
442+
jwtStr, err = dummyNode.InteractiveLoginWithMode(ctx, endpoints.AuthURL, endpoints.TokenURL, deviceAuthURL, controlPlaneInfo.ClientId, controlPlaneInfo.Audience, offlineAccessFlag, headlessFlag, mode)
433443
if err != nil {
434444
logger.Fatalf("Failed to get token: %v", err)
435445
}
@@ -550,6 +560,7 @@ func main() {
550560
rootCmd.PersistentFlags().StringVar(&audienceFlag, "audience", api.DefaultAudience, "OIDC Audience")
551561
rootCmd.PersistentFlags().StringVar(&dataDirFlag, "data-dir", "", "Override directory for the agent store (defaults to OS user config dir)")
552562
rootCmd.PersistentFlags().BoolVar(&headlessFlag, "headless", false, "Force headless out-of-band (OOB) authentication flow")
563+
rootCmd.PersistentFlags().StringVar(&authModeFlag, "auth-mode", "auto", "Interactive enrollment auth mode: auto, device, oob, or browser")
553564

554565
rootCmd.AddCommand(runCmd)
555566
rootCmd.AddCommand(joinCmd)

cmd/sam-node/main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var (
7373
audienceFlag string
7474
dataDirFlag string
7575
headlessFlag bool
76+
authModeFlag string
7677
daemonizeFlag bool
7778
resetAllFlag bool
7879
assumeYesFlag bool
@@ -276,12 +277,21 @@ func interactiveJoin(ctx context.Context, store *node.Store, targetControlPlane
276277
fmt.Printf("Client ID discovered: %s\n", info.ClientId)
277278

278279
logger.Info("Discovering OIDC endpoints...")
279-
tokenURL, authURL, err := dummyNode.DiscoverEndpoints(ctx, info.OidcIssuer)
280+
endpoints, err := dummyNode.DiscoverEndpointsWithDevice(ctx, info.OidcIssuer)
280281
if err != nil {
281282
return "", nil, fmt.Errorf("failed to discover OIDC endpoints: %w", err)
282283
}
284+
deviceAuthURL := endpoints.DeviceAuthURL
285+
if deviceAuthURLFlag != "" {
286+
deviceAuthURL = deviceAuthURLFlag
287+
}
288+
289+
mode, err := node.ParseAuthMode(authModeFlag)
290+
if err != nil {
291+
return "", nil, fmt.Errorf("invalid --auth-mode: %w", err)
292+
}
283293

284-
jwtStr, err := dummyNode.InteractiveLogin(ctx, authURL, tokenURL, info.ClientId, info.Audience, offlineAccessFlag, headlessFlag)
294+
jwtStr, err := dummyNode.InteractiveLoginWithMode(ctx, endpoints.AuthURL, endpoints.TokenURL, deviceAuthURL, info.ClientId, info.Audience, offlineAccessFlag, headlessFlag, mode)
285295
if err != nil {
286296
return "", nil, fmt.Errorf("failed to get token: %w", err)
287297
}
@@ -935,6 +945,7 @@ func main() {
935945
rootCmd.PersistentFlags().StringVar(&audienceFlag, "audience", api.DefaultAudience, "OIDC Audience")
936946
rootCmd.PersistentFlags().StringVar(&dataDirFlag, "data-dir", "", "Override directory for the agent store (defaults to OS user config dir)")
937947
rootCmd.PersistentFlags().BoolVar(&headlessFlag, "headless", false, "Force headless out-of-band (OOB) authentication flow")
948+
rootCmd.PersistentFlags().StringVar(&authModeFlag, "auth-mode", "auto", "Interactive enrollment auth mode: auto, device, oob, or browser")
938949

939950
rootCmd.AddCommand(runCmd)
940951
rootCmd.AddCommand(joinCmd)

0 commit comments

Comments
 (0)