feat: Native PKCE Auth for Tetrate via Custom URL Scheme#6445
feat: Native PKCE Auth for Tetrate via Custom URL Scheme#6445raj-subhankar wants to merge 20 commits intoblock:mainfrom
Conversation
24bc358 to
f6b9fd9
Compare
There was a problem hiding this comment.
Pull request overview
This PR implements native PKCE (Proof Key for Code Exchange) authentication for Tetrate using macOS's ASWebAuthenticationSession API, with a fallback to browser-based authentication on other platforms. The implementation eliminates the need for a temporary callback server by leveraging the native OS authentication flow.
Changes:
- Adds a new TypeScript authentication module implementing PKCE flow with state validation and timeout handling
- Implements a native macOS Node.js addon using Objective-C++ to interface with ASWebAuthenticationSession
- Updates Rust backend to support PKCE code verification through a new
/tetrate/verifyendpoint - Configures the macOS app bundle to handle
goose://URL scheme for OAuth callbacks
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/tetrateAuth.ts | Core authentication logic implementing PKCE flow, callback handling, and native session integration |
| ui/desktop/src/tetrateAuth.test.ts | Unit tests covering PKCE pair creation, URL parsing, and callback handling |
| ui/desktop/src/native/auth_session/src/auth_session.mm | Native macOS module wrapping ASWebAuthenticationSession with N-API bindings |
| ui/desktop/src/native/auth_session/binding.gyp | Build configuration for the native module targeting macOS 12.0+ |
| ui/desktop/scripts/build-auth-session.js | Build script to compile the native addon during development and packaging |
| ui/desktop/src/preload.ts | Adds IPC bridge for startTetrateAuth electron API |
| ui/desktop/src/main.ts | Integrates callback URL handling in protocol handler and adds IPC handler for auth initiation |
| ui/desktop/src/utils/tetrateSetup.ts | Simplified to call native auth through electron API instead of backend |
| ui/desktop/package.json | Adds native build scripts and @electron/node-gyp dependency |
| ui/desktop/forge.config.ts | Configures URL scheme handler and includes native modules in build artifacts |
| crates/goose/src/config/signup_tetrate/mod.rs | Adds static method for code exchange accepting external verifier |
| crates/goose-server/src/routes/setup.rs | Adds new verify endpoint accepting code and code_verifier |
| ui/desktop/openapi.json | OpenAPI schema additions for TetrateVerifyRequest endpoint |
| ui/desktop/src/api/*.gen.ts | Auto-generated API client types and functions |
Files not reviewed (1)
- ui/desktop/package-lock.json: Language not supported
53ce7e1 to
9336b80
Compare
| "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"], | ||
| "xcode_settings": { | ||
| "CLANG_ENABLE_OBJC_ARC": "YES", | ||
| "MACOSX_DEPLOYMENT_TARGET": "12.0", |
There was a problem hiding this comment.
Let's make sure this is aligned with the Mac OSX versions supported by goose.
There was a problem hiding this comment.
Goose uses Electron 39, which has a minimum macOS 12 requirement, so this should be fine.
| match TetrateAuth::exchange_code_with_verifier(request.code, request.code_verifier).await { | ||
| Ok(api_key) => api_key, | ||
| Err(e) => { | ||
| return Ok(Json(SetupResponse { |
There was a problem hiding this comment.
Should we return a different status code here rather than a 200 on failure?
There was a problem hiding this comment.
Returning 200 with success: false was an existing behavior and matches the pattern used by the Openrouter setup flow.
| static NSMutableSet<ASWebAuthenticationSession *> *gActiveSessions = nil; | ||
| static GooseAuthPresentationContextProvider *gPresentationContextProvider = nil; |
There was a problem hiding this comment.
Will these globals cause issues if multiple auth flows are triggered at the same time? Is it possible to trigger multiple at the same time?
There was a problem hiding this comment.
ASWebAuthenticationSession only supports a single login session at a time, so I think this should be fine.
|
oh interesting, looks like some merge conflicts (most are generated code though). I didn't know about this on macos - is there an example of this? seems a fair bit of work, but macos is pretty common so may be ok (OTOH spinning up a little server isn't the end of the world if smooth enough) |
66bd6e9 to
33a51fd
Compare
|
hey @michaelneale 👋 Yeah, spinning up a server for auth is totally fine. The main goal here was to keep the user in the app, instead of opening a browser tab and leaving them there. To make that work, we had to change the server based approach as well. Also I’ve fixed the conflicts and updated the PR description. |
|
yeah this did work when I tried it - is nice. May be overkill as I think previous was was similarly smooth (but with native browser I guess) |
5d502ad to
b074a82
Compare
|
@michaelneale I agree this is a significant change, and for most cases the previous login flow did feel smooth. That said, I do run into issues on when using Goose alongside local dev projects. Since Goose spins up the login server on localhost:3000, if you already have something running on that port the login just fails silently, with no indication of why. While we could fix this by not hardcoding the port, the new flow addresses it as well and has the added benefit of feeling more native. |
|
@raj-subhankar is there a way to do it with a dynamic port (or this macos native way) but use the native browser vs a window? |
|
@michaelneale Yes, we could do this with a custom URL scheme using the external browser. However, I’m advocating for ASWebAuthenticationSession because it’s the best of both worlds. To clarify: on macOS this uses the user’s default browser (Chrome, Safari, etc.), not an embedded webview, and it shares the browser’s cookies, so you still get the same SSO experience. The “window” you see is the OS delegating the auth request to the default browser. The key advantage is lifecycle management: the system lets us automatically dismiss the window as soon as authentication completes. With a external browser flow, the user is left with a “Success” tab they have to manually close before returning to the app. Given that this is Apple’s recommended approach for native app authentication, and the standard used by apps like Slack and VS Code, is there a specific UX concern you have with the modal window? |
f7d15a8 to
eecf26c
Compare
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com>
eecf26c to
5ba1688
Compare
|
🔍 Recipe Security Scan Results ✅ Status: APPROVED - All recipes passed security scan 📊 Scan Summary:
📋 Individual Recipe Results: 🔗 View detailed scan results in the workflow artifacts. |
ad83134 to
a807094
Compare
2210bca to
dc3f2f5
Compare
c48841f to
b25af59
Compare
|
I think we can close this, with a port fix and possibly #7166 for responding to low balance I think we could be in a good place. |
Summary
This PR moves Tetrate authentication from a server-driven flow (Rust PKCE + localhost callback) to a client-driven flow (Electron PKCE + goose:// deep-link callback).
It also adds a /tetrate/verify endpoint so the Electron client can send the authorization code and PKCE verifier for server-side token exchange.
Type of Change
AI Assistance
Testing
Related Issues
Relates to #ISSUE_ID
Discussion: LINK (if any)
Screenshots/Demos (for UX changes)
Before:
Goose.Tetrate.login.before.mov
After:
Custom.url.mov