fix: redirect OAuth2 authorization page to dashboard (#24499)

Currently when a user clicks either the Cancel or Allow button on the
authorization page the client app URI is executed but the page does not
land to the main dashboard page, leaving the two buttons open for
multiple clicks from the user. Aside from the potential problems it
might cause by activating the callback URI multiple times, the page also
provides poor UX because users usually expect the authorization tab to
return to the dashboard.

The consent page now executes the OAuth2 callback (auth code on Allow,
`access_denied` on Cancel) and hides the two buttons and updates the
existing description with a user instruction to close the window.
Initial implementation relied on a pop-up window executing the callback
while the main window was redirected to the dashboard main page.
- resolves https://github.com/coder/coder/issues/20323

<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->
This commit is contained in:
Faur Ioan-Aurel
2026-04-27 23:26:17 +03:00
committed by GitHub
parent ad3095106d
commit a8e7f329ac
4 changed files with 61 additions and 22 deletions
+4 -4
View File
@@ -175,10 +175,10 @@ func ShowAuthorizePage(accessURL *url.URL) http.HandlerFunc {
AppName: app.Name,
// #nosec G203 -- The scheme is validated by
// codersdk.ValidateRedirectURIScheme above.
CancelURI: htmltemplate.URL(cancelURI),
RedirectURI: r.URL.String(),
CSRFToken: nosurf.Token(r),
Username: ua.FriendlyName,
CancelURI: htmltemplate.URL(cancelURI),
DashboardURL: accessURL.String(),
CSRFToken: nosurf.Token(r),
Username: ua.FriendlyName,
})
}
}
+10 -7
View File
@@ -20,14 +20,17 @@ func TestOAuthConsentFormIncludesCSRFToken(t *testing.T) {
rec := httptest.NewRecorder()
site.RenderOAuthAllowPage(rec, req, site.RenderOAuthAllowData{
AppName: "Test OAuth App",
CancelURI: htmltemplate.URL("https://coder.com/cancel"),
RedirectURI: "https://coder.com/oauth2/authorize?client_id=test",
CSRFToken: csrfFieldValue,
Username: "test-user",
AppName: "Test OAuth App",
CancelURI: htmltemplate.URL("https://coder.com/cancel"),
DashboardURL: "https://coder.com/",
CSRFToken: csrfFieldValue,
Username: "test-user",
})
require.Equal(t, http.StatusOK, rec.Result().StatusCode)
assert.Contains(t, rec.Body.String(), `name="csrf_token"`)
assert.Contains(t, rec.Body.String(), `value="`+csrfFieldValue+`"`)
body := rec.Body.String()
assert.Contains(t, body, `name="csrf_token"`)
assert.Contains(t, body, `value="`+csrfFieldValue+`"`)
assert.Contains(t, body, `id="allow-form"`)
assert.Contains(t, body, `id="cancel-link"`)
}
+6 -6
View File
@@ -799,12 +799,12 @@ func (jfs justFilesSystem) Open(name string) (fs.File, error) {
// RenderOAuthAllowData contains the variables that are found in
// site/static/oauth2allow.html.
type RenderOAuthAllowData struct {
AppIcon string
AppName string
CancelURI htmltemplate.URL
RedirectURI string
CSRFToken string
Username string
AppIcon string
AppName string
CancelURI htmltemplate.URL
DashboardURL string
CSRFToken string
Username string
}
// RenderOAuthAllowPage renders the static page for a user to "Allow" an create
+41 -5
View File
@@ -64,7 +64,7 @@ links */}}
line-height: 140%;
}
.user-name {
.user-name {
font-weight: bold;
}
@@ -113,17 +113,53 @@ links */}}
<img class="coder-svg" src="/icon/coder.svg" alt="Coder" />
</div>
<h1>Authorize {{ .AppName }}</h1>
<p>
<p id="description">
Allow {{ .AppName }} to have full access to your
<span class="user-name">{{ .Username }}</span> account?
</p>
<div class="button-group">
<form method="POST" style="display: inline;">
<div class="button-group" id="button-group">
<form id="allow-form" method="POST" style="display: inline;">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}" />
<button type="submit" class="primary-button">Allow</button>
</form>
<a href="{{ .CancelURI }}">Cancel</a>
<a id="cancel-link" href="{{ .CancelURI }}">Cancel</a>
</div>
</div>
<script>
(function () {
var appName = "{{ .AppName }}";
var description = document.getElementById("description");
var buttonGroup = document.getElementById("button-group");
var allowForm = document.getElementById("allow-form");
var cancelLink = document.getElementById("cancel-link");
function showFeedback(message) {
buttonGroup.style.display = "none";
description.textContent = message;
}
allowForm.addEventListener("submit", function (e) {
e.preventDefault();
showFeedback(
appName +
" is now authorized to access your account. This window can now be closed."
);
setTimeout(function () {
allowForm.submit();
}, 500);
});
cancelLink.addEventListener("click", function (e) {
e.preventDefault();
showFeedback(
appName +
" was denied access to your account. This window can now be closed."
);
setTimeout(function () {
window.location.href = cancelLink.href;
}, 500);
});
})();
</script>
</body>
</html>