🔐 fix(oauth): stop authorize flow from bouncing to /console; respect next and redirect unauthenticated users to consent

Problem
- Starting OAuth from Discourse hit GET /api/oauth/authorize and 302’d to /login?next=/oauth/consent…
- The login page and AuthRedirect always navigated to /console when a session existed, ignoring next, which aborted the OAuth flow and dropped users in the console.

Changes
- Backend (src/oauth/server.go)
  - When not logged in, redirect directly to /oauth/consent?<original_query> instead of /login?next=…
  - Keep no-store headers; preserve the original authorize querystring.
- Frontend
  - web/src/helpers/auth.jsx: AuthRedirect now honors the login page’s next query param and only redirects to safe internal paths (starts with “/”, not “//”); otherwise falls back to /console.
  - web/src/components/auth/LoginForm.jsx: After successful login and after 2FA success, navigate to next when present and safe; otherwise go to /console.

Result
- The OAuth authorize flow now reliably reaches the consent screen.
- On approval, the server issues an authorization code and 302’s back to the client’s redirect_uri (e.g., Discourse), completing SSO as expected.

Security
- Sanitize next to avoid open-redirects by allowing only same-origin internal paths.

Compatibility
- No behavior change for normal username/password sign-ins outside the OAuth flow.
- No changes to token/userinfo endpoints.

Testing
- Manually verified end-to-end with Discourse OAuth2 Basic:
  - authorize → consent → approve → redirect with code
- Lint checks pass for modified files.
This commit is contained in:
t0ng7u
2025-09-25 13:02:40 +08:00
parent 63828349de
commit 380e1b7d56
3 changed files with 25 additions and 15 deletions

View File

@@ -176,7 +176,11 @@ const LoginForm = () => {
centered: true,
});
}
navigate('/console');
// 优先跳回 next仅允许相对路径
const sp = new URLSearchParams(window.location.search);
const next = sp.get('next');
const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//');
navigate(isSafeInternalPath ? next : '/console');
} else {
showError(message);
}
@@ -286,7 +290,10 @@ const LoginForm = () => {
setUserData(data);
updateAPI();
showSuccess('登录成功!');
navigate('/console');
const sp = new URLSearchParams(window.location.search);
const next = sp.get('next');
const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//');
navigate(isSafeInternalPath ? next : '/console');
};
// 返回登录页面

View File

@@ -36,7 +36,11 @@ export const AuthRedirect = ({ children }) => {
const user = localStorage.getItem('user');
if (user) {
return <Navigate to='/console' replace />;
// 优先使用登录页上的 next 参数(仅允许站内相对路径)
const sp = new URLSearchParams(window.location.search);
const next = sp.get('next');
const isSafeInternalPath = next && next.startsWith('/') && !next.startsWith('//');
return <Navigate to={isSafeInternalPath ? next : '/console'} replace />;
}
return children;