From a39fe8625f5822e85e5c4d73871357b1725cafbd Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sat, 16 May 2026 21:26:36 +0200 Subject: [PATCH 1/2] feat(controlplane): render branded HTML page with copy button for OIDC token Replace the plain-text token output with a self-contained HTML page when the OIDC callback flow has no client callback URL (CLI manual login). The page shows the JWT in a styled block and provides a Copy Token button that uses the Clipboard API. Cache-Control: no-store and Referrer-Policy: no-referrer prevent the bearer token from leaking via caches or the Referer header. The token stays in the response body and is never placed in the URL. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino --- app/controlplane/internal/service/auth.go | 6 +- .../internal/service/auth_token_page.go | 153 ++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 app/controlplane/internal/service/auth_token_page.go diff --git a/app/controlplane/internal/service/auth.go b/app/controlplane/internal/service/auth.go index e677d153c..45803a9ec 100644 --- a/app/controlplane/internal/service/auth.go +++ b/app/controlplane/internal/service/auth.go @@ -353,9 +353,11 @@ func callbackHandler(svc *AuthService, w http.ResponseWriter, r *http.Request) * callbackValue := callbackURLFromCookie.Value - // There is no callback, just render the token + // There is no callback, render the token in an HTML page with a copy button if callbackValue == "" { - fmt.Fprintf(w, "copy this token and paste it in your terminal window\n\n%s", userToken) + if err := renderTokenPage(w, userToken); err != nil { + return newOauthResp(http.StatusInternalServerError, err, false) + } return newOauthResp(http.StatusOK, nil, false) } diff --git a/app/controlplane/internal/service/auth_token_page.go b/app/controlplane/internal/service/auth_token_page.go new file mode 100644 index 000000000..58e4e31d8 --- /dev/null +++ b/app/controlplane/internal/service/auth_token_page.go @@ -0,0 +1,153 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "fmt" + "html/template" + "net/http" +) + +var tokenPageTemplate = template.Must(template.New("tokenPage").Parse(tokenPageHTML)) + +// renderTokenPage serves a self-contained HTML page that displays the JWT +// to the user with a copy-to-clipboard button. The token is rendered in the +// response body (never in the URL), and headers prevent caching or referrer +// leakage so the bearer token does not escape the page. +func renderTokenPage(w http.ResponseWriter, token string) error { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Referrer-Policy", "no-referrer") + + if err := tokenPageTemplate.Execute(w, struct{ Token string }{Token: token}); err != nil { + return fmt.Errorf("failed to render token page: %w", err) + } + return nil +} + +// #nosec G101 -- HTML template, not a credential +const tokenPageHTML = ` + + + + +Chainloop — Your authentication token + + + +
+

You're authenticated

+

Copy the token below and paste it into your terminal to complete the login.

+ {{.Token}} +
+ + +
+
+ + +` From 2b3abdd620eaf81242232c074a653a7a5c095b75 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Sat, 16 May 2026 21:29:20 +0200 Subject: [PATCH 2/2] fix(controlplane): copy token fallback for insecure (non-HTTPS) origins navigator.clipboard.writeText only works in a secure context (HTTPS or localhost), so it fails on plain HTTP deployments. Fall back to a hidden textarea + document.execCommand('copy') so the Copy Token button works on insecure origins as well. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino --- .../internal/service/auth_token_page.go | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/controlplane/internal/service/auth_token_page.go b/app/controlplane/internal/service/auth_token_page.go index 58e4e31d8..1e089c7d1 100644 --- a/app/controlplane/internal/service/auth_token_page.go +++ b/app/controlplane/internal/service/auth_token_page.go @@ -136,16 +136,40 @@ const tokenPageHTML = `