You're authenticated
+Copy the token below and paste it into your terminal to complete the login.
+{{.Token}}
+ 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..1e089c7d1 --- /dev/null +++ b/app/controlplane/internal/service/auth_token_page.go @@ -0,0 +1,177 @@ +// +// 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 = ` + +
+ + +Copy the token below and paste it into your terminal to complete the login.
+{{.Token}}
+