Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(frontend): reload the page on login / logout success #2432

Merged
merged 46 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
47880c1
refactor: prefer full page reload instead of AJAX request on logout
Lodin May 15, 2024
b0da734
Merge branch 'main' into fix/auth/logout
Lodin May 15, 2024
d0e51b0
Merge branch 'main' into fix/auth/logout
Lodin May 17, 2024
e4fe19f
Merge branch 'main' into fix/auth/logout
platosha May 21, 2024
520b7f9
Revert "refactor: prefer full page reload instead of AJAX request on …
platosha May 21, 2024
6684156
fix: reload the page on login / logout success
platosha May 21, 2024
59b3545
Merge branch 'main' into fix/auth/logout
platosha May 21, 2024
a95294d
fix: restore Spring CSRF handling
platosha May 21, 2024
16fe750
fix: normalize redirect as path
platosha May 21, 2024
77721ae
test: prevent flackyness of parallel security tests
platosha May 21, 2024
d0fce75
fix(frontend): support context path in redirects
platosha May 21, 2024
7fd761e
chore: cleanup
platosha May 21, 2024
8ab3f76
test(security): verify server-side logout redirect location
platosha May 21, 2024
5d679c5
test(security): make tests more reliable
platosha May 22, 2024
c3c5c23
fix: normalize default redirect URL
platosha May 22, 2024
19ab584
test(security): expect page reload from API
platosha May 22, 2024
918146b
chore: Java formatting
platosha May 22, 2024
a33a101
fix: more accurate normalize url
platosha May 22, 2024
17b2b0a
test(security): extra wait for page load
platosha May 22, 2024
160313a
test(security): extra wait for page load
platosha May 22, 2024
32f15f3
test(security): extra wait for page load
platosha May 22, 2024
c82a7b6
Merge branch 'main' into fix/auth/logout
taefi May 22, 2024
9ed6427
test(security): more explicit load waiting
platosha May 22, 2024
e5527be
chore: cleanup
platosha May 22, 2024
612c11a
test(security): fix load waiting
platosha May 22, 2024
fd28290
chore: Java formatting
platosha May 22, 2024
ed7d66d
test: add implicit wait
platosha May 22, 2024
6a5c8e7
test: rollback asserting path, use builtin wait for main-view
platosha May 22, 2024
1d37e7a
test: assertin path using script
platosha May 23, 2024
0df4b03
fix: reload with url mapping
platosha May 23, 2024
df1209f
test: wait for invalid session reloads
platosha May 23, 2024
d88bdea
chore: remove test logger
platosha May 23, 2024
5b04cad
test: extra wait on open
platosha May 23, 2024
6634358
Merge branch 'main' into fix/auth/logout
taefi May 24, 2024
6e44ebe
Merge branch 'main' into fix/auth/logout
platosha May 24, 2024
9ef8116
Merge branch 'main' into fix/auth/logout
platosha May 24, 2024
5b5ae3c
Revert "test: prevent flackyness of parallel security tests"
platosha May 24, 2024
a554a08
fix(frontend): prevent following requests when pageReloadNavigation i…
platosha May 24, 2024
0b81096
Revert "test: add implicit wait"
platosha May 24, 2024
7dc560c
feat!(frontend): onSuccess login / logout callbacks for custom after-…
platosha May 24, 2024
68209d5
test: use onSuccess callbacks
platosha May 24, 2024
8b2954f
Merge branch 'main' into fix/auth/logout
platosha May 24, 2024
c003e0b
test: wait for load when asserting current URL
platosha May 24, 2024
8be95eb
test: apply url mapping to logout path
platosha May 24, 2024
1dec469
chore(frontend): remove unnecessary .toString()
platosha May 27, 2024
4f49469
feat(frontend): support URL in login / logout options
platosha May 27, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const client = new ConnectClient({
prefix: '../connect',
middlewares: [
new InvalidSessionMiddleware(async () => {
// @ts-ignore
window.reloadPending = true;
location.reload();
return {
error: true,
Expand Down
31 changes: 18 additions & 13 deletions packages/java/tests/spring/security/frontend/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ export function isAuthenticated() {
* Uses `localStorage` for offline support.
*/
export async function login(username: string, password: string): Promise<LoginResult> {
const result = await loginImpl(username, password);
if (!result.error) {
// Get user info from endpoint
await appStore.fetchUserInfo();
authentication = {
timestamp: new Date().getTime(),
};
return await loginImpl(username, password, {
async onSuccess() {
// Get user info from endpoint
await appStore.fetchUserInfo();
authentication = {
timestamp: new Date().getTime(),
};

// Save the authentication to local storage
localStorage.setItem(AUTHENTICATION_KEY, JSON.stringify(authentication));
}
// Save the authentication to local storage
localStorage.setItem(AUTHENTICATION_KEY, JSON.stringify(authentication));

return result;
// @ts-ignore
window.reloadPending = true;
},
});
}

/**
Expand All @@ -69,6 +71,9 @@ export async function login(username: string, password: string): Promise<LoginRe
*/
export async function logout() {
setSessionExpired();
await logoutImpl();
appStore.clearUserInfo();
await logoutImpl({onSuccess() {
// @ts-ignore
window.reloadPending = true;
appStore.clearUserInfo();
}});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const client = new ConnectClient({
prefix: 'connect',
middlewares: [
new InvalidSessionMiddleware(async () => {
// @ts-ignore
window.reloadPending = true;
location.reload();
return {
error: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,27 @@ export class LoginView extends View implements AfterEnterObserver {
// If login was opened directly, use the default URL provided by the server.

// As we do not know if the target is a resource or a Fusion view or a Flow view, we cannot just use Router.go
window.location.href = result.redirectUrl || this.returnUrl || '';

// Navigation should happen automatically
// window.location.href = result.redirectUrl || this.returnUrl || '';
};

render() {
return html` <vaadin-login-overlay opened .error="${this.error}" @login="${this.login}"> </vaadin-login-overlay> `;
}

async login(event: CustomEvent): Promise<LoginResult> {
// @ts-ignore
window.reloadPending = true;
this.error = false;
const result = await login(event.detail.username, event.detail.password);
this.error = result.error;

if (!result.error) {
this.onSuccess(result);
} else {
// @ts-ignore
window.reloadPending = false;
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protected void configure(HttpSecurity http) throws Exception {
.permitAll();

super.configure(http);
setLoginView(http, "/login");
setLoginView(http, "/login", applyUrlMapping("/"));
http.logout().logoutUrl(applyUrlMapping("/logout"));

if (stateless) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ protected String getUrlMappingBasePath() {

protected void open(String path) {
getDriver().get(getRootURL() + getUrlMappingBasePath() + "/" + path);
waitForDocumentReady();
}

protected void openResource(String path) {
Expand Down Expand Up @@ -221,11 +222,13 @@ public void access_restricted_to_logged_in_users() {
openResource(path);
assertLoginViewShown();
loginUser();
assertResourceShown(path);
assertPageContains(contents);
logout();

openResource(path);
loginAdmin();
assertResourceShown(path);
assertPageContains(contents);
logout();

Expand All @@ -246,6 +249,7 @@ public void access_restricted_to_admin() {

openResource(path);
loginAdmin();
assertResourceShown(path);
String adminResult = getDriver().getPageSource();
Assert.assertTrue(adminResult.contains(contents));
logout();
Expand Down Expand Up @@ -307,8 +311,16 @@ protected void navigateTo(String path, boolean assertPathShown) {
}
}

protected void waitForDocumentReady() {
waitUntil(driver -> Boolean.TRUE
.equals(this.getCommandExecutor().executeScript(
"return !window.reloadPending && window.document.readyState "
+ "=== 'complete';")));
}

private TestBenchElement getMainView() {
return waitUntil(driver -> $("*").id("main-view"));
waitForDocumentReady();
return $("*").withId("main-view").waitForFirst();
}

protected void assertLoginViewShown() {
Expand All @@ -317,6 +329,7 @@ protected void assertLoginViewShown() {
}

private void assertRootPageShown() {
assertPathShown("");
waitUntil(drive -> $("h1").attribute("id", "header").exists());
String headerText = $("h1").id("header").getText();
Assert.assertEquals(ROOT_PAGE_HEADER_TEXT, headerText);
Expand All @@ -343,6 +356,7 @@ private void assertPathShown(String path) {
}

private void assertPathShown(String path, boolean includeUrlMapping) {
waitForDocumentReady();
waitUntil(driver -> {
String url = driver.getCurrentUrl();
String expected = getRootURL();
Expand Down Expand Up @@ -377,7 +391,9 @@ private void login(String username, String password) {
form.getUsernameField().setValue(username);
form.getPasswordField().setValue(password);
form.submit();
waitForDocumentReady();
waitUntilNot(driver -> $(LoginOverlayElement.class).exists());
waitForDocumentReady();
}

protected void refresh() {
Expand Down
95 changes: 88 additions & 7 deletions packages/ts/frontend/src/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
return token;
}

async function doLogout(logoutUrl: string, headers: Record<string, string>) {
async function doLogout(logoutUrl: URL | string, headers: Record<string, string>) {
const response = await fetch(logoutUrl, { headers, method: 'POST' });
if (!response.ok) {
throw new Error(`failed to logout with response ${response.status}`);
}

await updateCsrfTokensBasedOnResponse(response);

return response;
}

export interface LoginResult {
Expand All @@ -59,12 +61,73 @@
defaultUrl?: string;
}

export type SuccessCallback = () => Promise<void> | void;

export type NavigateFunction = (path: string) => void;

export interface LoginOptions {
loginProcessingUrl?: string;
/**
* The URL for login request, defaults to `/login`.
*/
loginProcessingUrl?: URL | string;

/**
* The success callback.
*/
onSuccess?: SuccessCallback;

/**
* The navigation callback, called after successful login. The default
* reloads the page.
*/
navigate?: NavigateFunction;
}

export interface LogoutOptions {
logoutUrl?: string;
/**
* The URL for logout request, defaults to `/logout`.
*/
logoutUrl?: URL | string;

/**
* The success callback.
*/
onSuccess?: SuccessCallback;

/**
* The navigation callback, called after successful logout. The default
* reloads the page.
*/
navigate?: NavigateFunction;
}

function normalizePath(url: string): string {
// URL with context path
const effectiveBaseURL = new URL('.', document.baseURI);
const effectiveBaseURI = effectiveBaseURL.toString();

let normalized = url;

// Strip context path prefix
if (normalized.startsWith(effectiveBaseURL.pathname)) {
return `/${normalized.slice(effectiveBaseURL.pathname.length)}`;
}

// Strip base URI
normalized = normalized.startsWith(effectiveBaseURI) ? `/${normalized.slice(effectiveBaseURI.length)}` : normalized;

return normalized;

Check warning on line 119 in packages/ts/frontend/src/Authentication.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/frontend/src/Authentication.ts#L119

Added line #L119 was not covered by tests
}

/**
* Navigates to the provided path using page reload.
*
* @param to - navigation target path
*/
function navigateWithPageReload(to: string) {
// Consider absolute path to be within application context
const url = to.startsWith('/') ? new URL(`.${to}`, document.baseURI) : to;
window.location.replace(url);

Check warning on line 130 in packages/ts/frontend/src/Authentication.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/frontend/src/Authentication.ts#L130

Added line #L130 was not covered by tests
}

/**
Expand Down Expand Up @@ -109,6 +172,15 @@
updateSpringCsrfMetaTags(springCsrfTokenInfo);
}

if (options?.onSuccess) {
await options.onSuccess();
}

const url = savedUrl ?? defaultUrl ?? document.baseURI;
const toPath = normalizePath(url);
const navigate = options?.navigate ?? navigateWithPageReload;
navigate(toPath);

return {
defaultUrl,
error: false,
Expand Down Expand Up @@ -141,23 +213,32 @@
export async function logout(options?: LogoutOptions): Promise<void> {
// this assumes the default Spring Security logout configuration (handler URL)
const logoutUrl = options?.logoutUrl ?? 'logout';
let response: Response | undefined;
try {
const headers = getSpringCsrfTokenHeadersForAuthRequest(document);
await doLogout(logoutUrl, headers);
response = await doLogout(logoutUrl, headers);
} catch {
try {
const response = await fetch('?nocache');
const responseText = await response.text();
const noCacheResponse = await fetch('?nocache');
const responseText = await noCacheResponse.text();
const doc = new DOMParser().parseFromString(responseText, 'text/html');
const headers = getSpringCsrfTokenHeadersForAuthRequest(doc);
await doLogout(logoutUrl, headers);
response = await doLogout(logoutUrl, headers);
} catch (error) {
// clear the token if the call fails
clearSpringCsrfMetaTags();
throw error;
}
} finally {
CookieManager.remove(JWT_COOKIE_NAME);
if (response && response.ok && response.redirected) {
if (options?.onSuccess) {
await options.onSuccess();
}
const toPath = normalizePath(response.url);
const navigate = options?.navigate ?? navigateWithPageReload;
navigate(toPath);
}
}
}

Expand Down
Loading
Loading