Skip to content

Add device flow authentication#18

Open
carole-lavillonniere wants to merge 5 commits intomainfrom
carole/drg-450
Open

Add device flow authentication#18
carole-lavillonniere wants to merge 5 commits intomainfrom
carole/drg-450

Conversation

@carole-lavillonniere
Copy link
Collaborator

@carole-lavillonniere carole-lavillonniere commented Feb 5, 2026

This PR adds a device code flow as a fallback option when the browser-based authentication doesn't work.

When running lstk start, the CLI now supports two authentication paths:

  1. Browser Flow (primary, introduced here Browser authentication flow #9): Opens browser and listens for callback on localhost:45678
  2. Device Flow (fallback, new in this PR): User presses ENTER to manually complete authentication using a verification code

User Experience

When running lstk:

Browser didn't open? Open https://app.localstack.cloud/auth/request/abc123 to authorize device.
Verification code: XYZ789
Waiting for authentication... (Press ENTER when complete)

If user presses ENTER after confirming in browser:

Checking if auth request is confirmed...
Auth request confirmed, exchanging for token...
Fetching license token...
Login successful!

Mock Server Approach

This PR introduces a platform API client.
The device flow tests use httptest.NewServer to create a real HTTP server that mimics the platform API.
The production code remains unchanged, we just point it to a different URL via LOCALSTACK_PLATFORM_URL.
This allows TestDeviceFlowSuccess to verify the complete end-to-end flow including container startup with a valid token.

if err := listener.Close(); err != nil {
log.Printf("failed to close listener: %v", err)
}
}()
Copy link
Collaborator Author

@carole-lavillonniere carole-lavillonniere Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.Shutdown() closes the listener, so no explicit close needed

func getWebAppURL() string {
// allows overriding the URL for testing
if url := os.Getenv("LOCALSTACK_WEB_APP_URL"); url != "" {
return url
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why I added this in the first place, but we never overwrite it at the moment, so I removed it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically we could use it to test against staging or locally started webapp.

I wonder though if we should maybe look into handling these envvars with the config package? I'll refactor it slightly later maybe

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Makes sense. Put it back in.
I agree it would be good to have a central place to read env vars and not do it all over the codebase.
I used this in the past https://github.com/caarlos0/env

assert.True(t, inspect.State.Running, "container should be running")
}

func TestStartCommandTriggersLoginWithoutToken(t *testing.T) {
Copy link
Collaborator Author

@carole-lavillonniere carole-lavillonniere Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was just moved to test/integration/login_browser_flow_test.go

@carole-lavillonniere carole-lavillonniere marked this pull request as ready for review February 6, 2026 11:03
Copy link
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some minor nits and questions :) But looks nice ✅

}

func NewPlatformClient() *PlatformClient {
baseURL := os.Getenv("LOCALSTACK_PLATFORM_URL")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We already have this in the original CLI as API_ENDPOINT (which is set to https://api.localstack.cloud/v1). Maybe we can keep this with that name here for consistency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! Renamed to LOCALSTACK_API_ENDPOINT


func (browserLogin) Login(ctx context.Context) (string, error) {
func startCallbackServer() (*http.Server, chan string, chan error, error) {
listener, err := net.Listen("tcp", "127.0.0.1:45678")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know it was in already before, but maybe we can have this address defined as a constant somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Added a constant

Comment on lines +99 to +104
// Listen for ENTER key in background
go func() {
reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n')
enterCh <- struct{}{}
}()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: here we'll have to check as well how to integrate it well with UI components

func getWebAppURL() string {
// allows overriding the URL for testing
if url := os.Getenv("LOCALSTACK_WEB_APP_URL"); url != "" {
return url
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically we could use it to test against staging or locally started webapp.

I wonder though if we should maybe look into handling these envvars with the config package? I'll refactor it slightly later maybe

time.Sleep(1 * time.Second)

// Simulate browser callback with mock token
resp, err := http.Get("http://127.0.0.1:45678/auth/success?token=mock-token")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: what happens if we use https://localhost.localstack.cloud instead of http://127.0.0.1:45678? could we still handle it then? I imagine in the browser we'd prefer to send to https

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback is local (browser → 127.0.0.1) and never goes on the public internet so no TLS should be fine. Also the localhost.localstack.cloud DNS is not yet set up during login, only when the container starts and it would route to the container, not the CLI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localhost.localstack.cloud is always set up to redirect to 127.0.0.1, that's how we've registered that domain. We only need the container for the subdomains afaik

I was just wondering if the browser would give any warnings if we didn't use https, but since it's local it should be fine, yes :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, I did not know it was a public DNS. So it should work as well 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants