Merge pull request #1857 from umbraco/feature/basic-auth

Feature: Login should support returnPath
This commit is contained in:
Jacob Overgaard
2024-05-22 14:31:32 +02:00
committed by GitHub
7 changed files with 147 additions and 16 deletions

View File

@@ -1,14 +1,10 @@
import {
UMB_AUTH_CONTEXT,
UMB_MODAL_APP_AUTH,
UMB_STORAGE_REDIRECT_URL,
type UmbUserLoginState,
} from '@umbraco-cms/backoffice/auth';
import { UMB_AUTH_CONTEXT, UMB_MODAL_APP_AUTH, type UmbUserLoginState } from '@umbraco-cms/backoffice/auth';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { setStoredPath } from '@umbraco-cms/backoffice/utils';
export class UmbAppAuthController extends UmbControllerBase {
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
@@ -71,7 +67,12 @@ export class UmbAppAuthController extends UmbControllerBase {
}
// Save the current state
sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, window.location.href);
let currentUrl = window.location.href;
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('returnPath')) {
currentUrl = decodeURIComponent(searchParams.get('returnPath') || currentUrl);
}
setStoredPath(currentUrl);
// Figure out which providers are available
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry));

View File

@@ -4,7 +4,7 @@ import { UmbAppContext } from './app.context.js';
import { UmbServerConnection } from './server-connection.js';
import { UmbAppAuthController } from './app-auth.controller.js';
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -18,6 +18,7 @@ import {
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { retrieveStoredPath } from '@umbraco-cms/backoffice/utils';
@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
@@ -87,13 +88,14 @@ export class UmbAppElement extends UmbLitElement {
this.observe(this.#authContext.authorizationSignal, () => {
// Redirect to the saved state or root
let currentRoute = '';
const savedRoute = sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL);
if (savedRoute) {
sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL);
currentRoute = savedRoute.endsWith('logout') ? currentRoute : savedRoute;
const url = retrieveStoredPath();
const isBackofficePath = url?.pathname.startsWith(this.backofficePath) ?? true;
if (isBackofficePath) {
history.replaceState(null, '', url?.toString() ?? '');
} else {
window.location.href = url?.toString() ?? this.backofficePath;
}
history.replaceState(null, '', currentRoute);
});
}
@@ -173,9 +175,13 @@ export class UmbAppElement extends UmbLitElement {
// Try to initialise the auth flow and get the runtime status
try {
// If the runtime level is "install" we should clear any cached tokens
// If the runtime level is "install" or ?status=false is set, we should clear any cached tokens
// else we should try and set the auth status
if (this.#serverConnection.getStatus() === RuntimeLevelModel.INSTALL) {
const searchParams = new URLSearchParams(window.location.search);
if (
(searchParams.has('status') && searchParams.get('status') === 'false') ||
this.#serverConnection.getStatus() === RuntimeLevelModel.INSTALL
) {
await this.#authContext.clearTokenStorage();
} else {
await this.#setAuthStatus();

View File

@@ -5,10 +5,12 @@ export * from './get-processed-image-url.function.js';
export * from './math/math.js';
export * from './object/deep-merge.function.js';
export * from './pagination-manager/pagination.manager.js';
export * from './path/ensure-local-path.function.js';
export * from './path/ensure-path-ends-with-slash.function.js';
export * from './path/path-decode.function.js';
export * from './path/path-encode.function.js';
export * from './path/path-folder-name.function.js';
export * from './path/stored-path.function.js';
export * from './path/umbraco-path.function.js';
export * from './path/url-pattern-to-string.function.js';
export * from './selection-manager/selection.manager.js';

View File

@@ -0,0 +1,26 @@
import { expect } from '@open-wc/testing';
import { ensureLocalPath } from './ensure-local-path.function.js';
describe('ensureLocalPath', () => {
it('should return the same URL if it is a local URL', () => {
const localUrl = new URL('/test', window.location.origin);
expect(ensureLocalPath(localUrl).href).to.eq(localUrl.href);
});
it('should return the fallback URL if the input URL is not a local URL', () => {
const nonLocalUrl = new URL('https://example.com/test');
const fallbackUrl = new URL('http://localhost/fallback');
expect(ensureLocalPath(nonLocalUrl, fallbackUrl).href).to.eq(fallbackUrl.href);
});
it('should return the same URL if it is a local path', () => {
const localPath = '/test';
expect(ensureLocalPath(localPath).pathname).to.eq(localPath);
});
it('should return the fallback URL if the input path is not a local path', () => {
const nonLocalPath = 'https://example.com/test';
const fallbackUrl = new URL('http://localhost/fallback');
expect(ensureLocalPath(nonLocalPath, fallbackUrl).href).to.eq(fallbackUrl.href);
});
});

View File

@@ -0,0 +1,10 @@
/**
* Ensure that the path is a local path.
*/
export function ensureLocalPath(path: URL | string, fallbackPath?: URL | string): URL {
const url = new URL(path, window.location.origin);
if (url.origin === window.location.origin) {
return url;
}
return fallbackPath ? new URL(fallbackPath) : new URL(window.location.origin);
}

View File

@@ -0,0 +1,56 @@
import { expect } from '@open-wc/testing';
import { retrieveStoredPath, setStoredPath } from './stored-path.function.js';
import { UMB_STORAGE_REDIRECT_URL } from '@umbraco-cms/backoffice/auth';
describe('retrieveStoredPath', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('should return a null if no path is stored', () => {
expect(retrieveStoredPath()).to.be.null;
});
it('should return the stored path if a path is stored', () => {
const testSafeUrl = new URL('/test', window.location.origin);
setStoredPath(testSafeUrl.toString());
expect(retrieveStoredPath()?.toString()).to.eq(testSafeUrl.toString());
});
it('should remove the stored path after it is retrieved', () => {
setStoredPath('/test');
retrieveStoredPath();
expect(sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL)).to.be.null;
});
it('should return null if the stored path ends with "logout"', () => {
setStoredPath('/logout');
expect(retrieveStoredPath()).to.be.null;
});
it('should not be possible to trick it with a fake URL', () => {
setStoredPath('//www.google.com');
expect(retrieveStoredPath()).to.be.null;
// also test setting it directly in sessionStorage (this will return the current path instead of the fake path)
sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, '//www.google.com');
expect(retrieveStoredPath()?.pathname).to.eq(window.location.pathname);
});
});
describe('setStoredPath', () => {
beforeEach(() => {
sessionStorage.clear();
});
it('should store a local path', () => {
const testSafeUrl = new URL('/test', window.location.origin);
setStoredPath(testSafeUrl.toString());
expect(sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL)).to.eq(testSafeUrl.toString());
});
it('should not store a non-local path', () => {
setStoredPath('https://example.com/test');
expect(sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL)).to.be.null;
});
});

View File

@@ -0,0 +1,30 @@
import { ensureLocalPath } from './ensure-local-path.function.js';
import { UMB_STORAGE_REDIRECT_URL } from '@umbraco-cms/backoffice/auth';
/**
* Retrieve the stored path from the session storage.
* @remark This is used to redirect the user to the correct page after login.
*/
export function retrieveStoredPath(): URL | null {
let currentRoute = '';
const savedRoute = sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL);
if (savedRoute) {
sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL);
currentRoute = savedRoute.endsWith('logout') ? currentRoute : savedRoute;
}
return currentRoute ? ensureLocalPath(currentRoute) : null;
}
/**
* Store the path in the session storage.
* @remark This is used to redirect the user to the correct page after login.
* @remark The path must be a local path, otherwise it is not stored.
*/
export function setStoredPath(path: string): void {
const url = new URL(path, window.location.origin);
if (url.origin !== window.location.origin) {
return;
}
sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, url.toString());
}