Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions pkg/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
_ "embed"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -45,6 +46,8 @@ const (

// OAuth scopes - openid for the MCP server flow
DefaultScope = "openid email"

oauthTokenRequestTimeout = 30 * time.Second
)

// OAuthConfig represents the OAuth2 configuration
Expand Down Expand Up @@ -311,7 +314,11 @@ func (oc *OAuthConfig) exchangeCodeForTokens(ctx context.Context, code, orgID st
opts = append(opts, oauth2.SetAuthURLParam("org_id", orgID))
}

token, err := oc.Config.Exchange(ctx, code, opts...)
exchangeCtx, cancel := context.WithTimeout(ctx, oauthTokenRequestTimeout)
defer cancel()
exchangeCtx = context.WithValue(exchangeCtx, oauth2.HTTPClient, newOAuthHTTPClient())

token, err := oc.Config.Exchange(exchangeCtx, code, opts...)
if err != nil {
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
}
Expand All @@ -326,6 +333,17 @@ func (oc *OAuthConfig) exchangeCodeForTokens(ctx context.Context, code, orgID st
}, nil
}

func newOAuthHTTPClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
// Keep token requests on HTTP/1.1; the auth endpoint can leave HTTP/2 requests pending.
transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}

return &http.Client{
Transport: transport,
Timeout: oauthTokenRequestTimeout,
}
}

// RefreshTokens refreshes the access token using the refresh token
func RefreshTokens(ctx context.Context, tokens *TokenStorage) (*TokenStorage, error) {
if tokens.RefreshToken == "" {
Expand All @@ -349,7 +367,7 @@ func RefreshTokens(ctx context.Context, tokens *TokenStorage) (*TokenStorage, er

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

client := &http.Client{}
client := newOAuthHTTPClient()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send refresh request: %w", err)
Expand Down
23 changes: 22 additions & 1 deletion pkg/auth/oauth_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package auth

import "testing"
import (
"net/http"
"testing"
)

func TestNewOAuthConfigUsesAuthOverrides(t *testing.T) {
t.Setenv("KERNEL_AUTH_BASE_URL", "https://auth.dev.onkernel.com/")
Expand Down Expand Up @@ -55,3 +58,21 @@ func TestLegacyTokenRefreshConfigUsesProdDefaults(t *testing.T) {
t.Fatalf("tokenOAuthClientID = %q, want %q", got, want)
}
}

func TestOAuthHTTPClientUsesHTTP11WithTimeout(t *testing.T) {
client := newOAuthHTTPClient()
if got, want := client.Timeout, oauthTokenRequestTimeout; got != want {
t.Fatalf("client timeout = %s, want %s", got, want)
}

transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("client transport type = %T, want *http.Transport", client.Transport)
}
if transport.TLSNextProto == nil {
t.Fatal("TLSNextProto is nil, want HTTP/2 disabled explicitly")
}
if _, ok := transport.TLSNextProto["h2"]; ok {
t.Fatal("TLSNextProto enables HTTP/2")
}
}
6 changes: 3 additions & 3 deletions pkg/auth/success.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>kernel cli - authenticated</title>
<title>kernel cli - callback received</title>
<link rel="icon" type="image/svg+xml" href="__KERNEL_FAVICON__">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<style>
Expand Down Expand Up @@ -88,8 +88,8 @@
<path d="M285.036 0C289.434 0 293 3.56173 293 7.95537C293 12.3491 289.434 15.9107 285.036 15.9107C280.638 15.9107 277.072 12.349 277.072 7.95537C277.072 3.56178 280.638 8.45042e-05 285.036 0Z" fill="#1c2024"/>
</svg>
</div>
<p class="title">authentication successful</p>
<p class="subtitle">you can close this window and return to your terminal.</p>
<p class="title">callback received</p>
<p class="subtitle">return to your terminal to finish signing in.</p>
<div class="check-container">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
Expand Down
Loading