Skip to content
Draft
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
86 changes: 81 additions & 5 deletions pkg/server/tokenrequest/tokenrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"path"
"strings"

"github.com/openshift/osincli"

Expand Down Expand Up @@ -144,9 +145,23 @@ func (t *tokenRequest) displayTokenPost(osinOAuthClient *osincli.Client, w http.
}

data.AccessToken = accessData.AccessToken
data.OcLoginCommand = formatOcLoginCommand(data.AccessToken, data.PublicMasterURL)
data.CurlCommand = formatCurlCommand(data.AccessToken, data.PublicMasterURL)
renderToken(w, data)
}

func formatOcLoginCommand(accessToken, publicMasterURL string) string {
return fmt.Sprintf("oc login --token=%s --server=%s", accessToken, publicMasterURL)
}

func formatCurlCommand(accessToken, publicMasterURL string) string {
return fmt.Sprintf(
`curl -H "Authorization: Bearer %s" "%s/apis/user.openshift.io/v1/users/~"`,
accessToken,
strings.TrimRight(publicMasterURL, "/"),
)
}

func displayTokenStart(osinOAuthClient *osincli.Client, w http.ResponseWriter, req *http.Request, data *sharedData) (*osincli.AuthorizeData, bool) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8")

Expand Down Expand Up @@ -179,6 +194,8 @@ type tokenData struct {
sharedData

AccessToken string
OcLoginCommand string
CurlCommand string
PublicMasterURL string
LogoutURL string
}
Expand Down Expand Up @@ -216,27 +233,86 @@ const cssStyle = `
pre { padding-left: 1em; border-radius: 5px; color: #003d6e; background-color: #EAEDF0; padding: 1.5em 0 1.5em 4.5em; white-space: normal; text-indent: -2em; }
a { color: #00f; text-decoration: none; }
a:hover { text-decoration: underline; }
button { background: none; border: none; color: #00f; text-decoration: none; font: inherit; padding: 0; }
button:hover { text-decoration: underline; cursor: pointer; }
button, .copy-button { background: none; border: none; color: #00f; text-decoration: none; font: inherit; padding: 0; }
button:hover, .copy-button:hover { text-decoration: underline; cursor: pointer; }
.copy-heading { display: inline; }
.copy-heading .copy-button { font-size: 0.85em; margin-left: 0.75em; }
@media (min-width: 768px) {
.nowrap { white-space: nowrap; }
}
</style>
`

const copyToClipboardScript = `
<script>
(function () {
var copyLabel = 'Copy to clipboard';
var copiedLabel = 'Copied';

function showCopied(button) {
button.textContent = copiedLabel;
button.setAttribute('aria-label', copiedLabel);
window.setTimeout(function () {
button.textContent = copyLabel;
button.setAttribute('aria-label', copyLabel);
}, 1250);
}

function fallbackCopy(text, button) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
if (document.execCommand('copy')) {
showCopied(button);
}
} finally {
document.body.removeChild(textarea);
}
}

function copyText(text, button) {
if (!text) {
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function () {
showCopied(button);
}).catch(function () {
fallbackCopy(text, button);
});
return;
}
fallbackCopy(text, button);
}

document.querySelectorAll('[data-copy-text]').forEach(function (button) {
button.addEventListener('click', function () {
copyText(button.getAttribute('data-copy-text'), button);
});
});
})();
</script>
`

var tokenTemplate = template.Must(template.New("tokenTemplate").Parse(
cssStyle + `
{{ if .Error }}
{{ .Error }}
{{ else }}
<h2>Your API token is</h2>
<h2 class="copy-heading">Your API token is <button type="button" class="copy-button" data-copy-text="{{.AccessToken}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<code>{{.AccessToken}}</code>

<h2>Log in with this token</h2>
<h2 class="copy-heading">Log in with this token <button type="button" class="copy-button" data-copy-text="{{.OcLoginCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<pre>oc login <span class="nowrap">--token={{.AccessToken}}</span> <span class="nowrap">--server={{.PublicMasterURL}}</span></pre>

<h3>Use this token directly against the API</h3>
<h3 class="copy-heading">Use this token directly against the API <button type="button" class="copy-button" data-copy-text="{{.CurlCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h3>
<pre>curl <span class="nowrap">-H "Authorization: Bearer {{.AccessToken}}"</span> <span class="nowrap">"{{.PublicMasterURL}}/apis/user.openshift.io/v1/users/~"</span></pre>
Comment on lines 302 to 314

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the rendered command examples identical to the copied text.

formatCurlCommand() normalizes a trailing slash, but Line 314 rebuilds the visible curl example from PublicMasterURL instead of CurlCommand. If the configured server URL ends with /, the button copies the normalized command while the on-page example shows //apis/.... Rendering the preformatted fields here avoids that drift for both commands.

Suggested fix
-  <pre>oc login <span class="nowrap">--token={{.AccessToken}}</span> <span class="nowrap">--server={{.PublicMasterURL}}</span></pre>
+  <pre>{{.OcLoginCommand}}</pre>
 
   <h3 class="copy-heading">Use this token directly against the API <button type="button" class="copy-button" data-copy-text="{{.CurlCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h3>
-  <pre>curl <span class="nowrap">-H "Authorization: Bearer {{.AccessToken}}"</span> <span class="nowrap">"{{.PublicMasterURL}}/apis/user.openshift.io/v1/users/~"</span></pre>
+  <pre>{{.CurlCommand}}</pre>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var tokenTemplate = template.Must(template.New("tokenTemplate").Parse(
cssStyle + `
{{ if .Error }}
{{ .Error }}
{{ else }}
<h2>Your API token is</h2>
<h2 class="copy-heading">Your API token is <button type="button" class="copy-button" data-copy-text="{{.AccessToken}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<code>{{.AccessToken}}</code>
<h2>Log in with this token</h2>
<h2 class="copy-heading">Log in with this token <button type="button" class="copy-button" data-copy-text="{{.OcLoginCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<pre>oc login <span class="nowrap">--token={{.AccessToken}}</span> <span class="nowrap">--server={{.PublicMasterURL}}</span></pre>
<h3>Use this token directly against the API</h3>
<h3 class="copy-heading">Use this token directly against the API <button type="button" class="copy-button" data-copy-text="{{.CurlCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h3>
<pre>curl <span class="nowrap">-H "Authorization: Bearer {{.AccessToken}}"</span> <span class="nowrap">"{{.PublicMasterURL}}/apis/user.openshift.io/v1/users/~"</span></pre>
var tokenTemplate = template.Must(template.New("tokenTemplate").Parse(
cssStyle + `
{{ if .Error }}
{{ .Error }}
{{ else }}
<h2 class="copy-heading">Your API token is <button type="button" class="copy-button" data-copy-text="{{.AccessToken}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<code>{{.AccessToken}}</code>
<h2 class="copy-heading">Log in with this token <button type="button" class="copy-button" data-copy-text="{{.OcLoginCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h2>
<pre>{{.OcLoginCommand}}</pre>
<h3 class="copy-heading">Use this token directly against the API <button type="button" class="copy-button" data-copy-text="{{.CurlCommand}}" aria-label="Copy to clipboard">Copy to clipboard</button></h3>
<pre>{{.CurlCommand}}</pre>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/server/tokenrequest/tokenrequest.go` around lines 302 - 314, The rendered
command examples in tokenTemplate are drifting from the copied text because the
curl example is rebuilt from PublicMasterURL instead of using the preformatted
CurlCommand, while the login example already uses OcLoginCommand. Update the
template to render the same command strings used for the copy buttons so the
visible examples always match the copied content, and verify the curl output
stays normalized when PublicMasterURL has a trailing slash.

` + copyToClipboardScript + `
{{ end }}

<br><br>
Expand Down
182 changes: 182 additions & 0 deletions pkg/server/tokenrequest/tokenrequest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package tokenrequest

import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/openshift/oauth-server/pkg/server/csrf"
)

func TestFormatOcLoginCommand(t *testing.T) {
token := "sha256~abc123"
server := "https://api.example.com:6443"

got := formatOcLoginCommand(token, server)
want := "oc login --token=sha256~abc123 --server=https://api.example.com:6443"
if got != want {
t.Fatalf("formatOcLoginCommand() = %q, want %q", got, want)
}
}

func TestFormatCurlCommand(t *testing.T) {
token := "sha256~abc123"
server := "https://api.example.com:6443/"

got := formatCurlCommand(token, server)
want := `curl -H "Authorization: Bearer sha256~abc123" "https://api.example.com:6443/apis/user.openshift.io/v1/users/~"`
if got != want {
t.Fatalf("formatCurlCommand() = %q, want %q", got, want)
}
}

func TestRenderTokenIncludesCopyToClipboardControls(t *testing.T) {
data := tokenData{
sharedData: sharedData{
RequestURL: "/oauth/token/request",
},
AccessToken: "sha256~token-value",
OcLoginCommand: formatOcLoginCommand("sha256~token-value", "https://api.example.com:6443"),
CurlCommand: formatCurlCommand("sha256~token-value", "https://api.example.com:6443"),
PublicMasterURL: "https://api.example.com:6443",
}

var buf bytes.Buffer
renderToken(&buf, data)
body := buf.String()

expectContains(t, body, []string{
"Your API token is",
"Log in with this token",
"Use this token directly against the API",
`class="copy-button"`,
`aria-label="Copy to clipboard"`,
`data-copy-text="sha256~token-value"`,
`data-copy-text="oc login --token=sha256~token-value --server=https://api.example.com:6443"`,
`data-copy-text="curl -H &#34;Authorization: Bearer sha256~token-value&#34; &#34;https://api.example.com:6443/apis/user.openshift.io/v1/users/~&#34;"`,
"navigator.clipboard",
"document.execCommand('copy')",
`<a href="/oauth/token/request">Request another token</a>`,
})

if strings.Count(body, `class="copy-button"`) != 3 {
t.Fatalf("expected 3 copy buttons, got %d in:\n%s", strings.Count(body, `class="copy-button"`), body)
}
}

func TestRenderTokenEscapesSpecialCharactersInCopyAttributes(t *testing.T) {
token := `sha256~test"onclick='alert(1)'`
server := "https://api.example.com:6443"

data := tokenData{
sharedData: sharedData{
RequestURL: "/oauth/token/request",
},
AccessToken: token,
OcLoginCommand: formatOcLoginCommand(token, server),
CurlCommand: formatCurlCommand(token, server),
PublicMasterURL: server,
}

var buf bytes.Buffer
renderToken(&buf, data)
body := buf.String()

if strings.Contains(body, `data-copy-text="sha256~test"onclick`) {
t.Fatalf("token copy attribute was not HTML-escaped:\n%s", body)
}
if strings.Contains(body, "<script>alert(1)</script>") {
t.Fatalf("unexpected unescaped script content in output:\n%s", body)
}
expectContains(t, body, []string{
`data-copy-text="sha256~test&#34;onclick=&#39;alert(1)&#39;"`,
})
}

func TestRenderTokenErrorStateOmitsCopyControls(t *testing.T) {
data := tokenData{
sharedData: sharedData{
Error: "Error checking token",
RequestURL: "/oauth/token/request",
},
}

var buf bytes.Buffer
renderToken(&buf, data)
body := buf.String()

expectContains(t, body, []string{
"Error checking token",
`<a href="/oauth/token/request">Request another token</a>`,
})
if strings.Contains(body, `class="copy-button"`) {
t.Fatalf("error state should not render copy buttons:\n%s", body)
}
}

func TestRenderFormDisplayTokenStepUnchanged(t *testing.T) {
data := formData{
sharedData: sharedData{
RequestURL: "/oauth/token/request",
},
Action: "https://oauth.example.com/oauth/token/display",
Code: "auth-code",
CSRF: "csrf-token",
}

var buf bytes.Buffer
renderForm(&buf, data)
body := buf.String()

expectContains(t, body, []string{
`action="https://oauth.example.com/oauth/token/display"`,
`name="code" value="auth-code"`,
`name="csrf" value="csrf-token"`,
"Display Token",
})
if strings.Contains(body, `class="copy-button"`) {
t.Fatalf("display token form should not include copy buttons:\n%s", body)
}
}

func TestDisplayTokenPostRejectsInvalidCSRF(t *testing.T) {
handler := &tokenRequest{
csrf: &csrf.FakeCSRF{Token: "expected-csrf"},
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/oauth/token/display", strings.NewReader("csrf=wrong"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

handler.displayTokenPost(nil, rec, req)

if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status %d, got %d: %s", http.StatusBadRequest, rec.Code, rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "Could not check CSRF token") {
t.Fatalf("unexpected body: %s", rec.Body.String())
}
}

func TestDisplayTokenRejectsUnsupportedMethod(t *testing.T) {
handler := &tokenRequest{}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/oauth/token/display", nil)
handler.displayToken(nil, rec, req)

if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected status %d, got %d: %s", http.StatusMethodNotAllowed, rec.Code, rec.Body.String())
}
}

func expectContains(t *testing.T, body string, expected []string) {
t.Helper()
for _, fragment := range expected {
if !strings.Contains(body, fragment) {
t.Fatalf("expected body to contain %q, got:\n%s", fragment, body)
}
}
}