Skip to content

Commit 009eb5c

Browse files
yasithdevclaude
andcommitted
feat: add devtunnel auth token refresh endpoint
Add POST /tunnels/devtunnels/auth-token API endpoint that allows CS-Bridge to push fresh Entra ID tokens before expiry. The SDK manager now reads the token dynamically via a closure, so token updates take effect without restarting linkspan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea7f5ff commit 009eb5c

File tree

3 files changed

+56
-7
lines changed

3 files changed

+56
-7
lines changed

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func main() {
152152
// Tunnel management
153153
api.HandleFunc("/tunnels/devtunnels", tunnel.ListDevTunnels).Methods("GET")
154154
api.HandleFunc("/tunnels/devtunnels", tunnel.CreateDevTunnel).Methods("POST")
155+
api.HandleFunc("/tunnels/devtunnels/auth-token", tunnel.RefreshDevTunnelAuthToken).Methods("POST")
155156
api.HandleFunc("/tunnels/devtunnels/{id}", tunnel.DeleteDevTunnel).Methods("DELETE")
156157

157158
api.HandleFunc("/tunnels/frp", tunnel.ListFRPTunnels).Methods("GET")

subsystems/tunnel/api.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,34 @@ func ListFRPTunnels(w http.ResponseWriter, r *http.Request) {
168168
utils.RespondJSON(w, http.StatusOK, FRPTunnelListResponse{FRPTunnelInfos: ts})
169169
}
170170

171+
// RefreshAuthTokenRequest is the JSON body for POST /tunnels/devtunnels/auth-token.
172+
type RefreshAuthTokenRequest struct {
173+
AuthToken string `json:"authToken"`
174+
}
175+
176+
// RefreshDevTunnelAuthToken handles POST /tunnels/devtunnels/auth-token.
177+
// CS-Bridge calls this to push a fresh Entra ID token before the previous one expires.
178+
func RefreshDevTunnelAuthToken(w http.ResponseWriter, r *http.Request) {
179+
var req RefreshAuthTokenRequest
180+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
181+
utils.RespondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
182+
return
183+
}
184+
_ = r.Body.Close()
185+
186+
if req.AuthToken == "" {
187+
utils.RespondJSON(w, http.StatusBadRequest, map[string]string{"error": "authToken is required"})
188+
return
189+
}
190+
191+
if err := UpdateAuthToken(req.AuthToken); err != nil {
192+
utils.RespondJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
193+
return
194+
}
195+
196+
utils.RespondJSON(w, http.StatusOK, map[string]string{"status": "ok"})
197+
}
198+
171199
// DeleteFRPTunnel handles DELETE /tunnels/frp/{id}.
172200
func DeleteFRPTunnel(w http.ResponseWriter, r *http.Request) {
173201
vars := mux.Vars(r)

subsystems/tunnel/devtunnel_sdk.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ var globalSDK *SDKManager
7878
// InitSDK initialises the SDK Manager with a Microsoft Entra ID (Azure AD) bearer
7979
// token. The manager is stored in the package-level globalSDK variable.
8080
// It is safe to call InitSDK multiple times; only the first call takes effect.
81+
// The token can be refreshed later via UpdateAuthToken.
8182
func InitSDK(authToken string) error {
8283
// Re-entrant guard: only initialise once per token value.
83-
// If the token changed the caller should create a new process.
8484
if globalSDK != nil {
8585
return nil
8686
}
@@ -95,9 +95,17 @@ func InitSDK(authToken string) error {
9595
{Name: "linkspan", Version: "0.1.0"},
9696
}
9797

98-
// tokenProvider returns the bearer token for every outgoing SDK request.
98+
sdk := &SDKManager{
99+
authToken: authToken,
100+
sdkTunnels: make(map[string]*tunnels.Tunnel),
101+
}
102+
103+
// tokenProvider reads from sdk.authToken so that UpdateAuthToken takes
104+
// effect for all subsequent SDK requests without re-initialisation.
99105
tokenProvider := func() string {
100-
return "Bearer " + authToken
106+
sdk.mu.Lock()
107+
defer sdk.mu.Unlock()
108+
return "Bearer " + sdk.authToken
101109
}
102110

103111
debugClient := &http.Client{Transport: &debugTransport{base: http.DefaultTransport}}
@@ -106,11 +114,23 @@ func InitSDK(authToken string) error {
106114
return fmt.Errorf("devtunnel sdk: create manager: %w", err)
107115
}
108116

109-
globalSDK = &SDKManager{
110-
manager: mgr,
111-
authToken: authToken,
112-
sdkTunnels: make(map[string]*tunnels.Tunnel),
117+
sdk.manager = mgr
118+
globalSDK = sdk
119+
return nil
120+
}
121+
122+
// UpdateAuthToken replaces the Entra ID bearer token used for all subsequent
123+
// SDK requests. This allows CS-Bridge to push a fresh token before the
124+
// previous one expires, without restarting linkspan.
125+
func UpdateAuthToken(newToken string) error {
126+
sdk, err := requireSDK()
127+
if err != nil {
128+
return err
113129
}
130+
sdk.mu.Lock()
131+
defer sdk.mu.Unlock()
132+
sdk.authToken = newToken
133+
log.Printf("devtunnel sdk: auth token updated (len=%d)", len(newToken))
114134
return nil
115135
}
116136

0 commit comments

Comments
 (0)