From 62a028dff93f900f42ceac204df7f31e3e8a0205 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 9 Apr 2024 13:22:34 +0200
Subject: [PATCH 01/36] initial work to log in without losing context
---
.../src/apps/app/app-auth.controller.ts | 15 ++--
.../src/apps/app/app.element.ts | 14 ++++
.../src/packages/core/auth/auth-flow.ts | 31 +++++++-
.../src/packages/core/auth/auth.context.ts | 4 +
.../auth/modals/umb-app-auth-modal.element.ts | 74 +++++++++++++++++--
.../auth/modals/umb-app-auth-modal.token.ts | 10 +--
.../umbraco-news-dashboard.element.ts | 58 +++++----------
7 files changed, 145 insertions(+), 61 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 78f61dd40b..d30e2e3cf0 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -85,7 +85,14 @@ export class UmbAppAuthController extends UmbControllerBase {
throw new Error('[Fatal] No auth providers available');
}
- if (availableProviders.length === 1) {
+ // Show the provider selection screen
+ const selected = await this.#showLoginModal(userLoginState);
+
+ if (!selected) {
+ return false;
+ }
+
+ /*if (availableProviders.length === 1) {
// One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider
this.#authContext.makeAuthorizationRequest();
} else {
@@ -103,7 +110,7 @@ export class UmbAppAuthController extends UmbControllerBase {
return false;
}
}
- }
+ }*/
return this.#updateState();
}
@@ -129,12 +136,10 @@ export class UmbAppAuthController extends UmbControllerBase {
.onSubmit()
.catch(() => undefined);
- if (!selected?.providerName) {
+ if (!selected) {
return false;
}
- this.#authContext.makeAuthorizationRequest(selected.providerName, selected.loginHint);
-
return true;
}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index e5a2a100ab..a6813a6310 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -51,6 +51,17 @@ export class UmbAppElement extends UmbLitElement {
path: 'install',
component: () => import('../installer/installer.element.js'),
},
+ {
+ path: 'oauth_complete',
+ resolve: () => {
+ // Complete oauth
+ console.log('congrats you hit the oauth complete');
+ this.#authContext?.completeAuthorizationRequest();
+
+ // Let the opener know that we are done
+ window.opener?.postMessage('oauth_complete', '*');
+ },
+ },
{
path: 'upgrade',
component: () => import('../upgrader/upgrader.element.js'),
@@ -152,6 +163,9 @@ export class UmbAppElement extends UmbLitElement {
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has('code')) {
this.#authContext?.completeAuthorizationRequest();
+
+ // Let the opener know that we are done
+ window.opener?.postMessage('oauth_complete', '*');
return;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 01b20582f9..06bed95bdb 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
+import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_STORAGE_REDIRECT_URL, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
import type { LocationLike, StringMap } from '@umbraco-cms/backoffice/external/openid';
import {
@@ -30,6 +31,7 @@ import {
TokenRequest,
TokenResponse,
} from '@umbraco-cms/backoffice/external/openid';
+import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
const requestor = new FetchRequestor();
@@ -42,6 +44,22 @@ class UmbNoHashQueryStringUtils extends BasicQueryStringUtils {
}
}
+class UmbLocationInterceptor implements LocationLike {
+ public redirect = new UmbStringState(undefined);
+ hash = '';
+ host = '';
+ origin = '';
+ hostname = '';
+ pathname = '';
+ port = '';
+ protocol = '';
+ search = '';
+
+ assign(url: string | URL): void {
+ this.redirect.setValue(url.toString());
+ }
+}
+
/**
* This class is used to handle the auth flow through any backend supporting OpenID Connect.
* It needs to know the server url, the client id, the redirect uri and the scope.
@@ -95,6 +113,7 @@ export class UmbAuthFlow {
// tokens
#refreshToken: string | undefined;
#accessTokenResponse: TokenResponse | undefined;
+ #locationInterceptor = new UmbLocationInterceptor();
constructor(
openIdConnectUrl: string,
@@ -119,7 +138,7 @@ export class UmbAuthFlow {
this.#authorizationHandler = new RedirectRequestHandler(
this.#storageBackend,
new UmbNoHashQueryStringUtils(),
- window.location,
+ this.#locationInterceptor,
);
// set notifier to deliver responses
@@ -149,11 +168,15 @@ export class UmbAuthFlow {
sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL);
currentRoute = savedRoute;
}
- history.replaceState(null, '', currentRoute);
+ //history.replaceState(null, '', currentRoute);
}
});
}
+ authRedirect() {
+ return this.#locationInterceptor.redirect.asObservable();
+ }
+
/**
* This method will initialize all the state needed for the auth flow.
*
@@ -193,7 +216,7 @@ export class UmbAuthFlow {
* @param identityProvider The identity provider to use for the authorization request.
* @param usernameHint (Optional) The username to use for the authorization request. It will be provided to the OpenID server as a hint.
*/
- makeAuthorizationRequest(identityProvider: string, usernameHint?: string): void {
+ makeAuthorizationRequest(identityProvider: string, usernameHint?: string) {
const extras: StringMap = { prompt: 'consent', access_type: 'offline' };
// If the identity provider is not 'Umbraco', we will add it to the extras.
@@ -221,6 +244,8 @@ export class UmbAuthFlow {
);
this.#authorizationHandler.performAuthorizationRequest(this.#configuration, request);
+
+ return this.#locationInterceptor.redirect.asObservable();
}
/**
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index cf99b23c35..328a8bbd18 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -15,6 +15,10 @@ export class UmbAuthContext extends UmbContextBase {
#isInitialized = new ReplaySubject(1);
readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized));
+ get authRedirect() {
+ return this.#authFlow.authRedirect();
+ }
+
#isBypassed = false;
#serverUrl;
#backofficePath;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 9fb5122908..8a0caf8a3c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -1,14 +1,18 @@
import { UmbModalBaseElement } from '../../modal/index.js';
+import { UMB_AUTH_CONTEXT } from '../auth.context.token.js';
import type { UmbModalAppAuthConfig, UmbModalAppAuthValue } from './umb-app-auth-modal.token.js';
-import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
+import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-app-auth-modal')
export class UmbAppAuthModalElement extends UmbModalBaseElement {
+ @state()
+ private _error?: string;
+
get props() {
return {
userLoginState: this.data?.userLoginState ?? 'loggingIn',
- onSubmit: this.onSubmit,
+ onSubmit: this.onSubmit.bind(this),
};
}
@@ -27,6 +31,35 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
+ this.#authContext = authContext;
+
+ // Observe the auth redirects
+ this.observe(
+ authContext.authRedirect,
+ (url) => {
+ if (url) {
+ this.#openWindow = window.open(url, '_blank');
+ }
+ },
+ '_redirect',
+ );
+ });
+ }
+
+ disconnectedCallback(): void {
+ super.disconnectedCallback();
+ window.removeEventListener('message', this.#onMessage);
+ }
render() {
return html`
@@ -35,6 +68,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement${this.localize.term('login_timeout')}
`
: ''}
+ ${this._error ? html`${this._error}
` : ''}
{
- this.value = { providerName };
- this._submitModal();
+ private onSubmit = async (providerName: string) => {
+ const authContext = await this.getContext(UMB_AUTH_CONTEXT);
+ const redirect = authContext.makeAuthorizationRequest(providerName);
+
+ this.observe(
+ redirect,
+ (url) => {
+ if (url) {
+ this.#openWindow = window.open(url, '_blank');
+ }
+ },
+ '_redirect',
+ );
};
+ async #onMessage(evt: MessageEvent) {
+ if (evt.data === 'oauth_complete') {
+ if (this.#openWindow) {
+ this.#openWindow.close();
+ }
+
+ // Refresh the state
+ await this.#authContext?.setInitialState();
+
+ // Test if we are authorized
+ const isAuthed = this.#authContext?.getIsAuthorized();
+ this.value = { success: isAuthed };
+ if (isAuthed) {
+ this._submitModal();
+ } else {
+ this._error = 'Failed to authenticate';
+ }
+ }
+ }
+
static styles = [
UmbTextStyles,
css`
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.token.ts
index 97dd990b68..c51eacd4bc 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.token.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.token.ts
@@ -7,16 +7,10 @@ export type UmbModalAppAuthConfig = {
export type UmbModalAppAuthValue = {
/**
- * The name of the provider that the user has selected to authenticate with.
+ * An indicator of whether the authentication was successful.
* @required
*/
- providerName?: string;
-
- /**
- * The login hint that the user has provided to the provider.
- * @optional
- */
- loginHint?: string;
+ success?: boolean;
};
export const UMB_MODAL_APP_AUTH = new UmbModalToken('Umb.Modal.AppAuth', {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts
index d9a5135dbb..3f605ebf38 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts
@@ -1,61 +1,39 @@
-import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
-import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
+import { UMB_AUTH_CONTEXT } from '../core/auth/auth.context.token.js';
+import { tryExecuteAndNotify } from '../core/resources/tryExecuteAndNotify.function.js';
+import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UserGroupResource, type UserGroupResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@customElement('umb-umbraco-news-dashboard')
export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
- #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
-
@state()
- private _name = '';
+ _groups: UserGroupResponseModel[] = [];
- constructor() {
- super();
- this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => {
- this.#currentUserContext = instance;
- this.#observeCurrentUser();
- });
+ async clearToken() {
+ const authContext = await this.getContext(UMB_AUTH_CONTEXT);
+ authContext.clearTokenStorage();
}
- #observeCurrentUser(): void {
- if (!this.#currentUserContext) return;
- this.observe(this.#currentUserContext.currentUser, (user) => {
- this._name = user?.name ?? '';
- });
+ async makeAuthorizedRequest() {
+ const { data } = await tryExecuteAndNotify(this, UserGroupResource.getUserGroup({ skip: 0, take: 10 }));
+
+ if (data) {
+ this._groups = data.items;
+ }
}
render() {
return html`
-
- Welcome, ${this._name}
-
-
- This is the beta version of Umbraco 14, where you can have a first-hand look at the new Backoffice.
-
- Please refer to the
- documentation to learn
- more about what is possible. Here you will find excellent tutorials, guides, and references to help you get
- started extending the Backoffice.
+ Clear all tokens
+ Make authorized request
+
+ ${this._groups.map((group) => html`
${group.name}
`)}
`;
}
-
- static styles = [
- UmbTextStyles,
- css`
- :host {
- display: block;
- padding: var(--uui-size-layout-1);
- }
- p {
- position: relative;
- }
- `,
- ];
}
export default UmbUmbracoNewsDashboardElement;
From 262f2ba2745fbb01b0041f4026aad56d85bd0a68 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:05:59 +0200
Subject: [PATCH 02/36] wait to refresh the state
---
.../src/apps/app/app-auth.controller.ts | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 29bbf70527..951965918b 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -91,7 +91,7 @@ export class UmbAppAuthController extends UmbControllerBase {
if (availableProviders.length === 1) {
// One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider
- this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName);
+ await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName);
return this.#updateState();
}
@@ -103,7 +103,7 @@ export class UmbAppAuthController extends UmbControllerBase {
if (redirectProvider) {
// Redirect directly to the provider
- this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName);
+ await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName);
return this.#updateState();
}
@@ -129,7 +129,7 @@ export class UmbAppAuthController extends UmbControllerBase {
const denyLocalLogin = availableProviders.some((provider) => provider.meta?.behavior?.denyLocalLogin);
if (denyLocalLogin) {
// Unregister the Umbraco provider
- umbExtensionsRegistry.unregister('Umb.AuthProviders.Umbraco');
+ umbExtensionsRegistry.exclude('Umb.AuthProviders.Umbraco');
}
// Show the provider selection screen
@@ -150,7 +150,7 @@ export class UmbAppAuthController extends UmbControllerBase {
.onSubmit()
.catch(() => undefined);
- if (!selected) {
+ if (!selected?.success) {
return false;
}
@@ -162,9 +162,6 @@ export class UmbAppAuthController extends UmbControllerBase {
throw new Error('[Fatal] Auth context is not available');
}
- // Reinitialize the auth flow (load the state from local storage)
- this.#authContext.setInitialState();
-
// The authorization flow is finished, so let the caller know if the user is authorized
return this.#authContext.getIsAuthorized();
}
From e10fb61c3a72866e451246a070e55881a1fc7bcb Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:09:03 +0200
Subject: [PATCH 03/36] add route for oauth_complete and complete the
authorization flow
---
.../src/apps/app/app.element.ts | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 50974b1f58..ad2c17af0b 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -59,13 +59,9 @@ export class UmbAppElement extends UmbLitElement {
},
{
path: 'oauth_complete',
- resolve: () => {
+ resolve: async () => {
// Complete oauth
- console.log('congrats you hit the oauth complete');
this.#authContext?.completeAuthorizationRequest();
-
- // Let the opener know that we are done
- window.opener?.postMessage('oauth_complete', '*');
},
},
{
@@ -172,15 +168,15 @@ export class UmbAppElement extends UmbLitElement {
}
#redirect() {
- // If there is a ?code parameter in the url, then we are in the middle of the oauth flow
- // and we need to complete the login (the authorization notifier will redirect after this is done
- // essentially hitting this method again)
+ /** If there is a ?code parameter in the url, then we are in the middle of the oauth flow
+ * and we need to complete the login (the authorization notifier will redirect after this is done
+ * essentially hitting this method again)
+ * @deprecated This is a legacy way of handling oauth flow, it should be removed once the backend starts sending the oauth_complete route
+ * TODO: Remove this when the backend sends the oauth_complete route
+ */
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has('code')) {
this.#authContext?.completeAuthorizationRequest();
-
- // Let the opener know that we are done
- window.opener?.postMessage('oauth_complete', '*');
return;
}
From 9002f3f74a5921e0d3b3ad49c3480cb7ebf3b122 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:27:53 +0200
Subject: [PATCH 04/36] wait for authorization requests before proceeding
---
src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 951965918b..293bc916ed 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -91,7 +91,7 @@ export class UmbAppAuthController extends UmbControllerBase {
if (availableProviders.length === 1) {
// One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider
- await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName);
+ await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName, true);
return this.#updateState();
}
@@ -103,7 +103,7 @@ export class UmbAppAuthController extends UmbControllerBase {
if (redirectProvider) {
// Redirect directly to the provider
- await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName);
+ await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName, true);
return this.#updateState();
}
From 289cbc2512f281ab27c240903f122ea713bb7d51 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:28:09 +0200
Subject: [PATCH 05/36] return the url to be able to make a choice
---
.../src/external/openid/redirect_based_handler.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/external/openid/redirect_based_handler.ts b/src/Umbraco.Web.UI.Client/src/external/openid/redirect_based_handler.ts
index 8344c28cf2..e9bb94a8a7 100644
--- a/src/Umbraco.Web.UI.Client/src/external/openid/redirect_based_handler.ts
+++ b/src/Umbraco.Web.UI.Client/src/external/openid/redirect_based_handler.ts
@@ -69,11 +69,11 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
this.storageBackend.setItem(authorizationServiceConfigurationKey(handle), JSON.stringify(configuration.toJson())),
]);
- persisted.then(() => {
+ return persisted.then(() => {
// make the redirect request
const url = this.buildRequestUrl(configuration, request);
log('Making a request to ', request, url);
- this.locationLike.assign(url);
+ return url;
});
}
From 3294d8c98333c72ed78364071f54b96ef6d38b3b Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:28:25 +0200
Subject: [PATCH 06/36] wait for the authorization signal after submitting a
login
---
.../auth/modals/umb-app-auth-modal.element.ts | 65 ++++---------------
1 file changed, 11 insertions(+), 54 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 32df7c60e6..88d04d69d2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -31,36 +31,15 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
- this.#authContext = authContext;
-
- // Observe the auth redirects
- this.observe(
- authContext.authRedirect,
- (url) => {
- if (url) {
- this.#openWindow = window.open(url, '_blank');
- }
- },
- '_redirect',
- );
+ this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
+ this.observe(context.authorizationSignal, () => {}, '_authorizationSignal');
});
}
- disconnectedCallback(): void {
- super.disconnectedCallback();
- window.removeEventListener('message', this.#onMessage);
- }
-
render() {
return html`
@@ -80,38 +59,16 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
- const redirect = authContext.makeAuthorizationRequest(providerName);
-
- this.observe(
- redirect,
- (url) => {
- if (url) {
- this.#openWindow = window.open(url, '_blank');
- }
- },
- '_redirect',
- );
- };
-
- async #onMessage(evt: MessageEvent) {
- if (evt.data === 'oauth_complete') {
- if (this.#openWindow) {
- this.#openWindow.close();
- }
-
- // Refresh the state
- await this.#authContext?.setInitialState();
-
- // Test if we are authorized
- const isAuthed = this.#authContext?.getIsAuthorized();
- this.value = { success: isAuthed };
- if (isAuthed) {
- this._submitModal();
- } else {
- this._error = 'Failed to authenticate';
- }
+ await authContext.makeAuthorizationRequest(providerName);
+ console.log('[AuthModal] Received authorization signal');
+ const isAuthed = authContext.getIsAuthorized();
+ this.value = { success: isAuthed };
+ if (isAuthed) {
+ this._submitModal();
+ } else {
+ this._error = 'Failed to authenticate';
}
- }
+ };
static styles = [
UmbTextStyles,
From 315e05b8555a08f8a984e30842c7eec187fd7d8b Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:29:07 +0200
Subject: [PATCH 07/36] add an authorization signal
---
.../src/packages/core/auth/auth-flow.ts | 50 ++++---------------
1 file changed, 9 insertions(+), 41 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index e9be945df9..1b07218c91 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -13,8 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
-import { UMB_STORAGE_REDIRECT_URL, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
+import { UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
import type { LocationLike, StringMap } from '@umbraco-cms/backoffice/external/openid';
import {
BaseTokenRequestHandler,
@@ -31,7 +30,7 @@ import {
TokenRequest,
TokenResponse,
} from '@umbraco-cms/backoffice/external/openid';
-import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
+import { Subject } from '@umbraco-cms/backoffice/external/rxjs';
const requestor = new FetchRequestor();
@@ -44,22 +43,6 @@ class UmbNoHashQueryStringUtils extends BasicQueryStringUtils {
}
}
-class UmbLocationInterceptor implements LocationLike {
- public redirect = new UmbStringState(undefined);
- hash = '';
- host = '';
- origin = '';
- hostname = '';
- pathname = '';
- port = '';
- protocol = '';
- search = '';
-
- assign(url: string | URL): void {
- this.redirect.setValue(url.toString());
- }
-}
-
/**
* This class is used to handle the auth flow through any backend supporting OpenID Connect.
* It needs to know the server url, the client id, the redirect uri and the scope.
@@ -113,7 +96,8 @@ export class UmbAuthFlow {
// tokens
#tokenResponse?: TokenResponse;
- #locationInterceptor = new UmbLocationInterceptor();
+
+ authorizationSignal = new Subject();
constructor(
openIdConnectUrl: string,
@@ -137,11 +121,7 @@ export class UmbAuthFlow {
this.#notifier = new AuthorizationNotifier();
this.#tokenHandler = new BaseTokenRequestHandler(requestor);
this.#storageBackend = new LocalStorageBackend();
- this.#authorizationHandler = new RedirectRequestHandler(
- this.#storageBackend,
- new UmbNoHashQueryStringUtils(),
- this.#locationInterceptor,
- );
+ this.#authorizationHandler = new RedirectRequestHandler(this.#storageBackend, new UmbNoHashQueryStringUtils());
// set notifier to deliver responses
this.#authorizationHandler.setAuthorizationNotifier(this.#notifier);
@@ -150,6 +130,7 @@ export class UmbAuthFlow {
this.#notifier.setAuthorizationListener(async (request, response, error) => {
if (error) {
console.error('Authorization error', error);
+ this.authorizationSignal.next();
throw error;
}
@@ -162,21 +143,10 @@ export class UmbAuthFlow {
await this.#makeTokenRequest(response.code, codeVerifier);
await this.performWithFreshTokens();
await this.#saveTokenState();
-
- // 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;
- }
- //history.replaceState(null, '', currentRoute);
}
- });
- }
- authRedirect() {
- return this.#locationInterceptor.redirect.asObservable();
+ this.authorizationSignal.next();
+ });
}
/**
@@ -246,9 +216,7 @@ export class UmbAuthFlow {
true,
);
- this.#authorizationHandler.performAuthorizationRequest(this.#configuration, request);
-
- return this.#locationInterceptor.redirect.asObservable();
+ return this.#authorizationHandler.performAuthorizationRequest(this.#configuration, request);
}
/**
From 094e5538f35aac875325981023ef155cc61b7706 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 12:29:31 +0200
Subject: [PATCH 08/36] listen for authorization signals before re-initialising
the token + open popup for login
---
.../src/packages/core/auth/auth.context.ts | 68 +++++++++++++++++--
1 file changed, 61 insertions(+), 7 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index 12422b77f4..51fe39049b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -1,12 +1,12 @@
import type { UmbBackofficeExtensionRegistry, ManifestAuthProvider } from '../extension-registry/index.js';
import { UmbAuthFlow } from './auth-flow.js';
-import { UMB_AUTH_CONTEXT } from './auth.context.token.js';
+import { UMB_AUTH_CONTEXT, UMB_STORAGE_TOKEN_RESPONSE_NAME } from './auth.context.token.js';
import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js';
import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
-import { ReplaySubject, filter, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
+import { ReplaySubject, filter, firstValueFrom, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export class UmbAuthContext extends UmbContextBase {
#isAuthorized = new UmbBooleanState(false);
@@ -15,8 +15,8 @@ export class UmbAuthContext extends UmbContextBase {
#isInitialized = new ReplaySubject(1);
readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized));
- get authRedirect() {
- return this.#authFlow.authRedirect();
+ get authorizationSignal() {
+ return this.#authFlow.authorizationSignal;
}
#isBypassed = false;
@@ -24,6 +24,9 @@ export class UmbAuthContext extends UmbContextBase {
#backofficePath;
#authFlow;
+ #authWindowProxy?: WindowProxy | null;
+ #previousAuthUrl?: string;
+
constructor(host: UmbControllerHost, serverUrl: string, backofficePath: string, isBypassed: boolean) {
super(host, UMB_AUTH_CONTEXT);
this.#isBypassed = isBypassed;
@@ -31,6 +34,37 @@ export class UmbAuthContext extends UmbContextBase {
this.#backofficePath = backofficePath;
this.#authFlow = new UmbAuthFlow(serverUrl, this.getRedirectUrl(), this.getPostLogoutRedirectUrl());
+
+ // Observe the authorization signal and close the auth window
+ this.observe(
+ this.authorizationSignal,
+ () => {
+ // Update the authorization state
+ this.getIsAuthorized();
+ },
+ '_authFlowAuthorizationSignal',
+ );
+
+ // Observe changes to local storage and update the authorization state
+ // This establishes the tab-to-tab communication
+ window.addEventListener('storage', this.#onStorageEvent.bind(this));
+ }
+
+ destroy(): void {
+ super.destroy();
+ window.removeEventListener('storage', this.#onStorageEvent.bind(this));
+ }
+
+ async #onStorageEvent(evt: StorageEvent) {
+ console.log('[AuthContext] Storage event', evt);
+ if (evt.key === UMB_STORAGE_TOKEN_RESPONSE_NAME) {
+ // Close any open auth windows
+ this.#authWindowProxy?.close();
+ // Refresh the local storage state into memory
+ await this.setInitialState();
+ // Let any auth listeners (such as the auth modal) know that the auth state has changed
+ this.authorizationSignal.next();
+ }
}
/**
@@ -38,8 +72,28 @@ export class UmbAuthContext extends UmbContextBase {
* @param identityProvider The provider to use for login. Default is 'Umbraco'.
* @param usernameHint The username hint to use for login.
*/
- makeAuthorizationRequest(identityProvider = 'Umbraco', usernameHint?: string) {
- return this.#authFlow.makeAuthorizationRequest(identityProvider, usernameHint);
+ async makeAuthorizationRequest(identityProvider = 'Umbraco', redirect?: boolean, usernameHint?: string) {
+ const redirectUrl = await this.#authFlow.makeAuthorizationRequest(identityProvider, usernameHint);
+ if (redirect) {
+ location.href = redirectUrl;
+ return;
+ }
+
+ if (!this.#authWindowProxy || this.#authWindowProxy.closed) {
+ // TODO: Add popup behavior configuration to the authProvider's manifest
+ this.#authWindowProxy = window.open(
+ redirectUrl,
+ 'umbracoAuthPopup',
+ 'popup,width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no',
+ );
+ } else if (this.#previousAuthUrl !== redirectUrl) {
+ this.#authWindowProxy = window.open(redirectUrl, 'umbracoAuthPopup');
+ this.#authWindowProxy?.focus();
+ }
+
+ this.#previousAuthUrl = redirectUrl;
+
+ return firstValueFrom(this.authorizationSignal);
}
/**
@@ -173,7 +227,7 @@ export class UmbAuthContext extends UmbContextBase {
}
getRedirectUrl() {
- return `${window.location.origin}${this.#backofficePath}`;
+ return `${window.location.origin}${this.#backofficePath}oauth_complete`;
}
getPostLogoutRedirectUrl() {
From b2a7234248fdf6037d7e0b84bb961eae1a19d329 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 14:56:03 +0200
Subject: [PATCH 09/36] cleanup modal
---
.../core/auth/modals/umb-app-auth-modal.element.ts | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 88d04d69d2..5338fb397e 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -32,14 +32,6 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
- this.observe(context.authorizationSignal, () => {}, '_authorizationSignal');
- });
- }
-
render() {
return html`
@@ -60,7 +52,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
await authContext.makeAuthorizationRequest(providerName);
- console.log('[AuthModal] Received authorization signal');
+
const isAuthed = authContext.getIsAuthorized();
this.value = { success: isAuthed };
if (isAuthed) {
From 1b4ca8dd68056820ed83e6d5e9a268356cdd232c Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 14:56:38 +0200
Subject: [PATCH 10/36] cleanup popup
---
.../src/packages/core/auth/auth.context.ts | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index 51fe39049b..f6496a3382 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -79,15 +79,16 @@ export class UmbAuthContext extends UmbContextBase {
return;
}
+ const popupTarget = 'umbracoAuthPopup';
+
if (!this.#authWindowProxy || this.#authWindowProxy.closed) {
- // TODO: Add popup behavior configuration to the authProvider's manifest
this.#authWindowProxy = window.open(
redirectUrl,
- 'umbracoAuthPopup',
- 'popup,width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no',
+ popupTarget,
+ 'width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no',
);
} else if (this.#previousAuthUrl !== redirectUrl) {
- this.#authWindowProxy = window.open(redirectUrl, 'umbracoAuthPopup');
+ this.#authWindowProxy = window.open(redirectUrl, popupTarget);
this.#authWindowProxy?.focus();
}
From 9088ea1129a7681d02c0133fdd0861b1b4e7f993 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 15:00:18 +0200
Subject: [PATCH 11/36] let the logout page listen for authorized events and
redirect back to the frontpage in case an event occurs
---
.../src/apps/app/app.element.ts | 12 ++++++++++++
src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts | 1 +
2 files changed, 13 insertions(+)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index ad2c17af0b..0f258c022d 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -17,6 +17,7 @@ import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
+import { filter, first } from '@umbraco-cms/backoffice/external/rxjs';
@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
@@ -74,6 +75,17 @@ export class UmbAppElement extends UmbLitElement {
resolve: () => {
this.#authContext?.clearTokenStorage();
this.#authController.makeAuthorizationRequest('loggedOut');
+
+ // Listen for the user to be authorized
+ this.#authContext?.isAuthorized
+ .pipe(
+ filter((x) => !!x),
+ first(),
+ )
+ .subscribe(() => {
+ // Redirect to the root
+ history.replaceState(null, '', '');
+ });
},
},
{
diff --git a/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts b/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts
index cc5b1fcd73..24fa9d1a5a 100644
--- a/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts
@@ -18,4 +18,5 @@ export {
filter,
startWith,
skip,
+ first,
} from 'rxjs';
From 743ca6be04fd11ab64f507ddb06ac34a474086b3 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 15:37:31 +0200
Subject: [PATCH 12/36] resolve to let the flow go through, but will of course
fail because the token is invalid
---
.../src/packages/core/auth/auth-flow.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 1b07218c91..58f547d8a8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -304,10 +304,9 @@ export class UmbAuthFlow {
return Promise.resolve(this.#tokenResponse.accessToken);
}
- // if the refresh token is not set (maybe the provider doesn't support them), sign out
+ // if the refresh token is not set (maybe the provider doesn't support them)
if (!this.#tokenResponse?.refreshToken) {
- this.signOut();
- return Promise.reject('Missing refreshToken.');
+ return Promise.resolve('Missing refreshToken.');
}
const request = new TokenRequest({
From bc6b6f2e99944b366a5e9601d4843e3a0b353d76 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 15:37:44 +0200
Subject: [PATCH 13/36] remove stored url
---
.../src/apps/app/app-auth.controller.ts | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 293bc916ed..462e05ef6b 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -1,9 +1,4 @@
-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 { filter, firstValueFrom, skip } from '@umbraco-cms/backoffice/external/rxjs';
@@ -66,11 +61,6 @@ export class UmbAppAuthController extends UmbControllerBase {
throw new Error('[Fatal] Auth context is not available');
}
- // Save location.href so we can redirect to it after login
- if (location.href !== this.#authContext.getPostLogoutRedirectUrl()) {
- window.sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, location.href);
- }
-
// Figure out which providers are available
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry));
From b6a623922fefa703e8a62d73aa52e7b62bada327 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 15:50:24 +0200
Subject: [PATCH 14/36] destroy the auth modal if repeated events occur
---
.../src/apps/app/app-auth.controller.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 462e05ef6b..197c6b12be 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -123,15 +123,17 @@ export class UmbAppAuthController extends UmbControllerBase {
}
// Show the provider selection screen
+ const authModalKey = 'umbAuthModal';
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
- modalManager.remove('umbAuthModal');
+ modalManager.close(authModalKey);
+ modalManager.remove(authModalKey);
const selected = await modalManager
.open(this._host, UMB_MODAL_APP_AUTH, {
data: {
userLoginState,
},
modal: {
- key: 'umbAuthModal',
+ key: authModalKey,
backdropBackground: this.#firstTimeLoggingIn
? 'var(--umb-auth-backdrop, url("/umbraco/backoffice/assets/umbraco_logo_white.svg") 20px 20px / 200px no-repeat, radial-gradient(circle, rgba(2,0,36,1) 0%, rgba(40,58,151,.9) 50%, rgba(0,212,255,1) 100%))'
: 'var(--umb-auth-backdrop-timedout, rgba(0,0,0,0.75))',
From af91f95f1c00ac39bf2e263ac010d13cd94807a5 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 23 Apr 2024 15:54:09 +0200
Subject: [PATCH 15/36] use a timeout subject to consistently open the auth
modal
---
.../src/apps/app/app-auth.controller.ts | 10 ++--------
.../src/packages/core/auth/auth.context.ts | 7 ++++++-
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 197c6b12be..982824f5c2 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -18,12 +18,7 @@ export class UmbAppAuthController extends UmbControllerBase {
// Observe the user's authorization state and start the authorization flow if the user is not authorized
this.observe(
- context.isAuthorized.pipe(
- // Skip the first since it is always false
- skip(1),
- // Only continue if the value is false
- filter((x) => !x),
- ),
+ context.isTimeout,
() => {
this.#firstTimeLoggingIn = false;
this.makeAuthorizationRequest('timedOut');
@@ -125,8 +120,7 @@ export class UmbAppAuthController extends UmbControllerBase {
// Show the provider selection screen
const authModalKey = 'umbAuthModal';
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
- modalManager.close(authModalKey);
- modalManager.remove(authModalKey);
+
const selected = await modalManager
.open(this._host, UMB_MODAL_APP_AUTH, {
data: {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index f6496a3382..e366d31955 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -6,12 +6,16 @@ import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
-import { ReplaySubject, filter, firstValueFrom, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
+import { ReplaySubject, Subject, filter, firstValueFrom, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export class UmbAuthContext extends UmbContextBase {
#isAuthorized = new UmbBooleanState(false);
readonly isAuthorized = this.#isAuthorized.asObservable();
+ // Timeout is different from `isAuthorized` because it can occur repeatedly
+ #isTimeout = new Subject();
+ readonly isTimeout = this.#isTimeout.asObservable();
+
#isInitialized = new ReplaySubject(1);
readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized));
@@ -162,6 +166,7 @@ export class UmbAuthContext extends UmbContextBase {
timeOut() {
this.clearTokenStorage();
this.#isAuthorized.setValue(false);
+ this.#isTimeout.next();
}
/**
From 058ca6acc2cc63c6fff7805aabbf7aaf0d8d004a Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:20:56 +0200
Subject: [PATCH 16/36] add jsdoc
---
.../src/packages/core/auth/auth.context.ts | 25 +++++++++++++++----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index e366d31955..a9cc8ad18c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -6,18 +6,32 @@ import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
-import { ReplaySubject, Subject, filter, firstValueFrom, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
+import { ReplaySubject, Subject, firstValueFrom, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export class UmbAuthContext extends UmbContextBase {
#isAuthorized = new UmbBooleanState(false);
+
+ /**
+ * Observable that emits true if the user is authorized, otherwise false.
+ * @remark It will only emit when the authorization state changes.
+ */
readonly isAuthorized = this.#isAuthorized.asObservable();
// Timeout is different from `isAuthorized` because it can occur repeatedly
#isTimeout = new Subject();
+
+ /**
+ * Observable that emits when the user has timed out, i.e. the token has expired.
+ * This can be used to show a timeout message to the user.
+ * @remark It can emit multiple times if more than one request is made after the token has expired.
+ */
readonly isTimeout = this.#isTimeout.asObservable();
- #isInitialized = new ReplaySubject(1);
- readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized));
+ /**
+ * Observable that emits true when the auth context is initialized.
+ * @remark It will only emit once and then complete itself.
+ */
+ #isInitialized = new ReplaySubject(1);
get authorizationSignal() {
return this.#authFlow.authorizationSignal;
@@ -223,11 +237,12 @@ export class UmbAuthContext extends UmbContextBase {
}
setInitialized() {
- this.#isInitialized.next(true);
+ this.#isInitialized.next();
+ this.#isInitialized.complete();
}
getAuthProviders(extensionsRegistry: UmbBackofficeExtensionRegistry) {
- return this.isInitialized.pipe(
+ return this.#isInitialized.pipe(
switchMap(() => extensionsRegistry.byType<'authProvider', ManifestAuthProvider>('authProvider')),
);
}
From 4177b038fd6d42a198e6fb436738e29d3fac57da Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:23:48 +0200
Subject: [PATCH 17/36] order properties and rename isTimeout to timeoutSignal
---
.../src/apps/app/app-auth.controller.ts | 2 +-
.../src/packages/core/auth/auth-flow.ts | 4 ++
.../src/packages/core/auth/auth.context.ts | 40 +++++++++----------
3 files changed, 25 insertions(+), 21 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 982824f5c2..19d7e98dc3 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -18,7 +18,7 @@ export class UmbAppAuthController extends UmbControllerBase {
// Observe the user's authorization state and start the authorization flow if the user is not authorized
this.observe(
- context.isTimeout,
+ context.timeoutSignal,
() => {
this.#firstTimeLoggingIn = false;
this.makeAuthorizationRequest('timedOut');
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 58f547d8a8..89e4f0a1e0 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -97,6 +97,10 @@ export class UmbAuthFlow {
// tokens
#tokenResponse?: TokenResponse;
+ /**
+ * This signal will emit when the authorization flow is complete.
+ * @remark It will also emit if there is an error during the authorization flow.
+ */
authorizationSignal = new Subject();
constructor(
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index a9cc8ad18c..a9aeae560e 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -10,33 +10,13 @@ import { ReplaySubject, Subject, firstValueFrom, switchMap } from '@umbraco-cms/
export class UmbAuthContext extends UmbContextBase {
#isAuthorized = new UmbBooleanState(false);
-
- /**
- * Observable that emits true if the user is authorized, otherwise false.
- * @remark It will only emit when the authorization state changes.
- */
- readonly isAuthorized = this.#isAuthorized.asObservable();
-
// Timeout is different from `isAuthorized` because it can occur repeatedly
#isTimeout = new Subject();
-
- /**
- * Observable that emits when the user has timed out, i.e. the token has expired.
- * This can be used to show a timeout message to the user.
- * @remark It can emit multiple times if more than one request is made after the token has expired.
- */
- readonly isTimeout = this.#isTimeout.asObservable();
-
/**
* Observable that emits true when the auth context is initialized.
* @remark It will only emit once and then complete itself.
*/
#isInitialized = new ReplaySubject(1);
-
- get authorizationSignal() {
- return this.#authFlow.authorizationSignal;
- }
-
#isBypassed = false;
#serverUrl;
#backofficePath;
@@ -45,6 +25,26 @@ export class UmbAuthContext extends UmbContextBase {
#authWindowProxy?: WindowProxy | null;
#previousAuthUrl?: string;
+ /**
+ * Observable that emits true if the user is authorized, otherwise false.
+ * @remark It will only emit when the authorization state changes.
+ */
+ readonly isAuthorized = this.#isAuthorized.asObservable();
+
+ /**
+ * Observable that acts as a signal and emits when the user has timed out, i.e. the token has expired.
+ * This can be used to show a timeout message to the user.
+ * @remark It can emit multiple times if more than one request is made after the token has expired.
+ */
+ readonly timeoutSignal = this.#isTimeout.asObservable();
+
+ /**
+ * Observable that acts as a signal for when the authorization state changes.
+ */
+ get authorizationSignal() {
+ return this.#authFlow.authorizationSignal;
+ }
+
constructor(host: UmbControllerHost, serverUrl: string, backofficePath: string, isBypassed: boolean) {
super(host, UMB_AUTH_CONTEXT);
this.#isBypassed = isBypassed;
From a8f299d8a88f903fa1f2f86aa530db3e34b26145 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:24:07 +0200
Subject: [PATCH 18/36] remove console log
---
src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index a9aeae560e..b36f832037 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -74,7 +74,6 @@ export class UmbAuthContext extends UmbContextBase {
}
async #onStorageEvent(evt: StorageEvent) {
- console.log('[AuthContext] Storage event', evt);
if (evt.key === UMB_STORAGE_TOKEN_RESPONSE_NAME) {
// Close any open auth windows
this.#authWindowProxy?.close();
From 7caa2d30e22f879bf741dc39299256b419212ef3 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:27:59 +0200
Subject: [PATCH 19/36] add login hints
---
.../packages/core/auth/modals/umb-app-auth-modal.element.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 5338fb397e..be1ec0a969 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -49,9 +49,9 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
+ private onSubmit = async (providerName: string, loginHint?: string) => {
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
- await authContext.makeAuthorizationRequest(providerName);
+ await authContext.makeAuthorizationRequest(providerName, false, loginHint);
const isAuthed = authContext.getIsAuthorized();
this.value = { success: isAuthed };
From 244ce6a9daa0149a6e7a4a4a2dbb2f1929f413e3 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:42:59 +0200
Subject: [PATCH 20/36] let the manifest decide a few things about the popup
---
.../src/packages/core/auth/auth.context.ts | 20 ++++++++++++-------
.../auth-provider-default.element.ts | 11 +++++-----
.../auth/modals/umb-app-auth-modal.element.ts | 11 +++++++---
.../src/packages/core/auth/types.ts | 9 ++++++++-
.../models/auth-provider.model.ts | 15 ++++++++++++++
5 files changed, 50 insertions(+), 16 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index b36f832037..3ce24cd508 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -87,23 +87,29 @@ export class UmbAuthContext extends UmbContextBase {
/**
* Initiates the login flow.
* @param identityProvider The provider to use for login. Default is 'Umbraco'.
+ * @param redirect If true, the user will be redirected to the login page.
* @param usernameHint The username hint to use for login.
+ * @param manifest The manifest for the registered provider.
*/
- async makeAuthorizationRequest(identityProvider = 'Umbraco', redirect?: boolean, usernameHint?: string) {
+ async makeAuthorizationRequest(
+ identityProvider = 'Umbraco',
+ redirect?: boolean,
+ usernameHint?: string,
+ manifest?: ManifestAuthProvider,
+ ) {
const redirectUrl = await this.#authFlow.makeAuthorizationRequest(identityProvider, usernameHint);
if (redirect) {
location.href = redirectUrl;
return;
}
- const popupTarget = 'umbracoAuthPopup';
+ const popupTarget = manifest?.meta?.behavior?.popupTarget ?? 'umbracoAuthPopup';
+ const popupFeatures =
+ manifest?.meta?.behavior?.popupFeatures ??
+ 'width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no';
if (!this.#authWindowProxy || this.#authWindowProxy.closed) {
- this.#authWindowProxy = window.open(
- redirectUrl,
- popupTarget,
- 'width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no',
- );
+ this.#authWindowProxy = window.open(redirectUrl, popupTarget, popupFeatures);
} else if (this.#previousAuthUrl !== redirectUrl) {
this.#authWindowProxy = window.open(redirectUrl, popupTarget);
this.#authWindowProxy?.focus();
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
index c6c10745c1..eaa9cfe801 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
@@ -1,5 +1,5 @@
import type { ManifestAuthProvider } from '../../extension-registry/models/index.js';
-import type { UmbAuthProviderDefaultProps } from '../types.js';
+import type { UmbAuthProviderDefaultProps, UmbUserLoginState } from '../types.js';
import { UmbLitElement } from '../../lit-element/lit-element.element.js';
import { UmbTextStyles } from '../../style/index.js';
import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
@@ -7,10 +7,11 @@ import { css, customElement, html, nothing, property } from '@umbraco-cms/backof
@customElement('umb-auth-provider-default')
export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbAuthProviderDefaultProps {
@property({ attribute: false })
- manifest!: ManifestAuthProvider;
-
+ userLoginState?: UmbUserLoginState | undefined;
@property({ attribute: false })
- onSubmit!: (providerName: string, loginHint?: string) => void;
+ manifest!: ManifestAuthProvider;
+ @property({ attribute: false })
+ onSubmit!: (manifestOrProviderName: string | ManifestAuthProvider, loginHint?: string) => void;
connectedCallback(): void {
super.connectedCallback();
@@ -21,7 +22,7 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA
return html`
this.onSubmit(this.manifest.forProviderName)}
+ @click=${() => this.onSubmit(this.manifest)}
id="auth-provider-button"
.label=${this.manifest.meta?.label ?? this.manifest.forProviderName}
.look=${this.manifest.meta?.defaultView?.look ?? 'outline'}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index be1ec0a969..3f136123cc 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -1,6 +1,8 @@
+import type { ManifestAuthProvider } from '../../extension-registry/models/auth-provider.model.js';
import { UmbModalBaseElement } from '../../modal/index.js';
import { UmbTextStyles } from '../../style/text-style.style.js';
import { UMB_AUTH_CONTEXT } from '../auth.context.token.js';
+import type { UmbAuthProviderDefaultProps } from '../types.js';
import type { UmbModalAppAuthConfig, UmbModalAppAuthValue } from './umb-app-auth-modal.token.js';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
@@ -9,7 +11,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
+ private onSubmit = async (providerOrManifest: string | ManifestAuthProvider, loginHint?: string) => {
const authContext = await this.getContext(UMB_AUTH_CONTEXT);
- await authContext.makeAuthorizationRequest(providerName, false, loginHint);
+ const manifest = typeof providerOrManifest === 'string' ? undefined : providerOrManifest;
+ const providerName =
+ typeof providerOrManifest === 'string' ? providerOrManifest : providerOrManifest.forProviderName;
+ await authContext.makeAuthorizationRequest(providerName, false, loginHint, manifest);
const isAuthed = authContext.getIsAuthorized();
this.value = { success: isAuthed };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
index 7074b49380..9ee412c6f1 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
@@ -21,6 +21,13 @@ export interface UmbAuthProviderDefaultProps {
* Callback that is called when the user selects a provider.
* @param providerName The name of the provider that the user selected.
* @param loginHint The login hint to use for login if available.
+ * @deprecated Use the manifest parameter instead.
*/
- onSubmit: (providerName: string, loginHint?: string) => void;
+ onSubmit(providerName: string, loginHint?: string): void;
+
+ /**
+ * Callback that is called when the user selects a provider.
+ * @param manifest The manifest of the provider that the user selected.
+ */
+ onSubmit(manifest: ManifestAuthProvider): void;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
index 116be49a73..85771d103b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
@@ -65,6 +65,21 @@ export interface MetaAuthProvider {
* @default false
*/
autoRedirect?: boolean;
+
+ /**
+ * The target of the popup that is opened when the user logs in.
+ * @default 'umbracoAuthPopup'
+ * @remarks This is the name of the window that is opened when the user logs in, use `_blank` to open in a new tab.
+ */
+ popupTarget?: string;
+
+ /**
+ * The features of the popup that is opened when the user logs in.
+ * @default 'width=600,height=600,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,toolbar=no'
+ * @remarks This is the features of the window that is opened when the user logs in.
+ * @seehref https://developer.mozilla.org/en-US/docs/Web/API/Window/open#features
+ */
+ popupFeatures?: string;
};
/**
From a0528197faf5da1342aca845637684516c7af3e5 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:45:29 +0200
Subject: [PATCH 21/36] add loginhint to second impl
---
src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
index 9ee412c6f1..b8b01291d6 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/types.ts
@@ -29,5 +29,5 @@ export interface UmbAuthProviderDefaultProps {
* Callback that is called when the user selects a provider.
* @param manifest The manifest of the provider that the user selected.
*/
- onSubmit(manifest: ManifestAuthProvider): void;
+ onSubmit(manifest: ManifestAuthProvider, loginHint?: string): void;
}
From 90baa782deb18d9b1fae9a63737cead0d670a7bf Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 26 Apr 2024 15:45:34 +0200
Subject: [PATCH 22/36] add a try/catch
---
.../auth/modals/umb-app-auth-modal.element.ts | 32 ++++++++++++-------
1 file changed, 21 insertions(+), 11 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 3f136123cc..3245dee788 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -52,18 +52,28 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement {
- const authContext = await this.getContext(UMB_AUTH_CONTEXT);
- const manifest = typeof providerOrManifest === 'string' ? undefined : providerOrManifest;
- const providerName =
- typeof providerOrManifest === 'string' ? providerOrManifest : providerOrManifest.forProviderName;
- await authContext.makeAuthorizationRequest(providerName, false, loginHint, manifest);
+ try {
+ const authContext = await this.getContext(UMB_AUTH_CONTEXT);
+ if (!authContext) {
+ throw new Error('Auth context not available');
+ }
- const isAuthed = authContext.getIsAuthorized();
- this.value = { success: isAuthed };
- if (isAuthed) {
- this._submitModal();
- } else {
- this._error = 'Failed to authenticate';
+ const manifest = typeof providerOrManifest === 'string' ? undefined : providerOrManifest;
+ const providerName =
+ typeof providerOrManifest === 'string' ? providerOrManifest : providerOrManifest.forProviderName;
+
+ await authContext.makeAuthorizationRequest(providerName, false, loginHint, manifest);
+
+ const isAuthed = authContext.getIsAuthorized();
+ this.value = { success: isAuthed };
+ if (isAuthed) {
+ this._submitModal();
+ } else {
+ this._error = 'Failed to authenticate';
+ }
+ } catch (error) {
+ console.error('[AuthModal] Error submitting auth request', error);
+ this._error = error instanceof Error ? error.message : 'Unknown error (see console)';
}
};
From 8ef4b98dba47a3138951a26a25fc44abdb69fb00 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Mon, 29 Apr 2024 15:58:34 +0200
Subject: [PATCH 23/36] do not use the "oauth_complete" page before its
implemented on the backend
---
.../src/packages/core/auth/auth.context.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index 3ce24cd508..a0839a2712 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -253,7 +253,7 @@ export class UmbAuthContext extends UmbContextBase {
}
getRedirectUrl() {
- return `${window.location.origin}${this.#backofficePath}oauth_complete`;
+ return `${window.location.origin}${this.#backofficePath}`;
}
getPostLogoutRedirectUrl() {
From 5fd24e6a86248047ee7984151629b7007a74d49f Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Mon, 29 Apr 2024 16:51:11 +0200
Subject: [PATCH 24/36] use oauth_complete to handle authentication process
---
.../src/apps/app/app.element.ts | 24 ++++++++-----------
.../src/packages/core/auth/auth.context.ts | 2 +-
2 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 0f258c022d..64720dea81 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -60,8 +60,16 @@ export class UmbAppElement extends UmbLitElement {
},
{
path: 'oauth_complete',
- resolve: async () => {
- // Complete oauth
+ component: () => import('./app-error.element.js'),
+ setup: (component) => {
+ const searchParams = new URLSearchParams(window.location.search);
+ const hasCode = searchParams.has('code');
+ (component as UmbAppErrorElement).errorHeadline = 'Umbraco Authorization';
+ (component as UmbAppErrorElement).errorMessage = hasCode
+ ? 'Authorization completed. You may now close this window.'
+ : 'Authorization failed. Please try again.';
+
+ // Complete the authorization request
this.#authContext?.completeAuthorizationRequest();
},
},
@@ -180,18 +188,6 @@ export class UmbAppElement extends UmbLitElement {
}
#redirect() {
- /** If there is a ?code parameter in the url, then we are in the middle of the oauth flow
- * and we need to complete the login (the authorization notifier will redirect after this is done
- * essentially hitting this method again)
- * @deprecated This is a legacy way of handling oauth flow, it should be removed once the backend starts sending the oauth_complete route
- * TODO: Remove this when the backend sends the oauth_complete route
- */
- const queryParams = new URLSearchParams(window.location.search);
- if (queryParams.has('code')) {
- this.#authContext?.completeAuthorizationRequest();
- return;
- }
-
switch (this.#serverConnection?.getStatus()) {
case RuntimeLevelModel.INSTALL:
history.replaceState(null, '', 'install');
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index a0839a2712..3ce24cd508 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -253,7 +253,7 @@ export class UmbAuthContext extends UmbContextBase {
}
getRedirectUrl() {
- return `${window.location.origin}${this.#backofficePath}`;
+ return `${window.location.origin}${this.#backofficePath}oauth_complete`;
}
getPostLogoutRedirectUrl() {
From 827e41c0aaeaa241edbbc0e8a7afb57176f88cb5 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 10:33:00 +0200
Subject: [PATCH 25/36] add case if there is no opener
---
src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 64720dea81..7912440acb 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -70,7 +70,12 @@ export class UmbAppElement extends UmbLitElement {
: 'Authorization failed. Please try again.';
// Complete the authorization request
- this.#authContext?.completeAuthorizationRequest();
+ this.#authContext?.completeAuthorizationRequest().then(() => {
+ // If we don't have an opener, redirect to the root
+ if (!window.opener) {
+ history.replaceState(null, '', '/');
+ }
+ });
},
},
{
From c5efe5b49eeede2f66661383cf872572b3033f63 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 10:33:17 +0200
Subject: [PATCH 26/36] redirect in every case
---
src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 7912440acb..a4c9e3507a 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -70,7 +70,7 @@ export class UmbAppElement extends UmbLitElement {
: 'Authorization failed. Please try again.';
// Complete the authorization request
- this.#authContext?.completeAuthorizationRequest().then(() => {
+ this.#authContext?.completeAuthorizationRequest().finally(() => {
// If we don't have an opener, redirect to the root
if (!window.opener) {
history.replaceState(null, '', '/');
From 227e48bc9e3b6c2fb5fdbb2e3dc5f4b5ec769ad0 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 10:35:36 +0200
Subject: [PATCH 27/36] add docs for `oauth_complete`
---
src/Umbraco.Web.UI.Client/docs/authentication.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/docs/authentication.md b/src/Umbraco.Web.UI.Client/docs/authentication.md
index eaf53f619b..a713a9a19e 100644
--- a/src/Umbraco.Web.UI.Client/docs/authentication.md
+++ b/src/Umbraco.Web.UI.Client/docs/authentication.md
@@ -36,7 +36,9 @@ There are two ways to use this:
"CMS": {
"Security":{
"BackOfficeHost": "http://localhost:5173",
- "AuthorizeCallbackPathName": "/"
+ "AuthorizeCallbackPathName": "/oauth_complete",
+ "AuthorizeCallbackLogoutPathName": "/logout",
+ "AuthorizeCallbackErrorPathName": "/error",
},
},
[...]
From 0329fb78f7fefcc6d550ac402c07942149119c0d Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 10:42:57 +0200
Subject: [PATCH 28/36] remove option 'denyLocalLogin' since packages can do
that by excluding the umbraco provider in a backofficeEntryPoint
---
.../src/apps/app/app-auth.controller.ts | 16 +++-------------
.../models/auth-provider.model.ts | 6 ------
2 files changed, 3 insertions(+), 19 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index e7a6354505..0d77446596 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -65,7 +65,7 @@ export class UmbAppAuthController extends UmbControllerBase {
// If the user is timed out, we can show the login modal directly
if (userLoginState === 'timedOut') {
- const selected = await this.#showLoginModal(userLoginState, availableProviders);
+ const selected = await this.#showLoginModal(userLoginState);
if (!selected) {
return false;
@@ -93,7 +93,7 @@ export class UmbAppAuthController extends UmbControllerBase {
}
// Show the provider selection screen
- const selected = await this.#showLoginModal(userLoginState, availableProviders);
+ const selected = await this.#showLoginModal(userLoginState);
if (!selected) {
return false;
@@ -102,21 +102,11 @@ export class UmbAppAuthController extends UmbControllerBase {
return this.#updateState();
}
- async #showLoginModal(
- userLoginState: UmbUserLoginState,
- availableProviders: Array,
- ): Promise {
+ async #showLoginModal(userLoginState: UmbUserLoginState): Promise {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
- // Check if any provider denies local login
- const denyLocalLogin = availableProviders.some((provider) => provider.meta?.behavior?.denyLocalLogin);
- if (denyLocalLogin) {
- // Unregister the Umbraco provider
- umbExtensionsRegistry.exclude('Umb.AuthProviders.Umbraco');
- }
-
// Show the provider selection screen
const authModalKey = 'umbAuthModal';
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
index 85771d103b..9bd0244c57 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/auth-provider.model.ts
@@ -54,12 +54,6 @@ export interface MetaAuthProvider {
* The behavior of the provider when it is used.
*/
behavior?: {
- /**
- * If true, the Umbraco backoffice login will be disabled.
- * @default false
- */
- denyLocalLogin?: boolean;
-
/**
* If true, the user will be redirected to the provider's login page immediately.
* @default false
From cafb606e65181a8ec3e8733f1164b80ba8519c5b Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 11:11:42 +0200
Subject: [PATCH 29/36] do not distinguish between layouts
---
.../src/apps/app/app-auth.controller.ts | 9 ++-------
.../src/apps/app/app.element.ts | 2 +-
.../auth/modals/umb-app-auth-modal.element.ts | 19 +------------------
3 files changed, 4 insertions(+), 26 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index 0d77446596..ef690d37fc 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -1,14 +1,12 @@
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 { filter, firstValueFrom, skip } from '@umbraco-cms/backoffice/external/rxjs';
+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 type { ManifestAuthProvider } from '@umbraco-cms/backoffice/extension-registry';
export class UmbAppAuthController extends UmbControllerBase {
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
- #firstTimeLoggingIn = true;
constructor(host: UmbControllerHost) {
super(host);
@@ -20,7 +18,6 @@ export class UmbAppAuthController extends UmbControllerBase {
this.observe(
context.timeoutSignal,
() => {
- this.#firstTimeLoggingIn = false;
this.makeAuthorizationRequest('timedOut');
},
'_authState',
@@ -118,9 +115,7 @@ export class UmbAppAuthController extends UmbControllerBase {
},
modal: {
key: authModalKey,
- backdropBackground: this.#firstTimeLoggingIn
- ? 'var(--umb-auth-backdrop, rgb(244, 244, 244))'
- : 'var(--umb-auth-backdrop-timedout, rgba(244, 244, 244, 0.75))',
+ backdropBackground: 'var(--umb-auth-backdrop, rgb(244, 244, 244))',
},
})
.onSubmit()
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index a4c9e3507a..8805c793c8 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -67,7 +67,7 @@ export class UmbAppElement extends UmbLitElement {
(component as UmbAppErrorElement).errorHeadline = 'Umbraco Authorization';
(component as UmbAppErrorElement).errorMessage = hasCode
? 'Authorization completed. You may now close this window.'
- : 'Authorization failed. Please try again.';
+ : 'Authorization failed. Please close this window and try again.';
// Complete the authorization request
this.#authContext?.completeAuthorizationRequest().finally(() => {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
index 80be46d65b..d652b7b065 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts
@@ -36,7 +36,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement
+

Date: Tue, 30 Apr 2024 11:37:30 +0200
Subject: [PATCH 30/36] implement a way to listen if entrypoints have been
loaded
---
.../extension-initializer-base.ts | 19 +++++++++++++------
.../app-entry-point-extension-initializer.ts | 2 +-
...ffice-entry-point-extension-initializer.ts | 2 +-
3 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.ts
index 39b67f29c4..d79d971922 100644
--- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/initializers/extension-initializer-base.ts
@@ -4,6 +4,7 @@ import type { SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
+import { ReplaySubject } from '@umbraco-cms/backoffice/external/rxjs';
/**
* Base class for extension initializers, which are responsible for loading and unloading extensions.
@@ -15,12 +16,14 @@ export abstract class UmbExtensionInitializerBase<
protected host;
protected extensionRegistry;
#extensionMap = new Map();
+ #loaded = new ReplaySubject
(1);
+ loaded = this.#loaded.asObservable();
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry, manifestType: Key) {
super(host);
this.host = host;
this.extensionRegistry = extensionRegistry;
- this.observe(extensionRegistry.byType(manifestType), (extensions) => {
+ this.observe(extensionRegistry.byType(manifestType), async (extensions) => {
this.#extensionMap.forEach((existingExt) => {
if (!extensions.find((b) => b.alias === existingExt.alias)) {
this.unloadExtension(existingExt);
@@ -28,11 +31,15 @@ export abstract class UmbExtensionInitializerBase<
}
});
- extensions.forEach((extension) => {
- if (this.#extensionMap.has(extension.alias)) return;
- this.#extensionMap.set(extension.alias, extension);
- this.instantiateExtension(extension);
- });
+ await Promise.all(
+ extensions.map((extension) => {
+ if (this.#extensionMap.has(extension.alias)) return;
+ this.#extensionMap.set(extension.alias, extension);
+ return this.instantiateExtension(extension);
+ }),
+ );
+
+ this.#loaded.next();
});
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/app-entry-point-extension-initializer.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/app-entry-point-extension-initializer.ts
index 877b118b79..022fb492c2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/app-entry-point-extension-initializer.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/app-entry-point-extension-initializer.ts
@@ -32,7 +32,7 @@ export class UmbAppEntryPointExtensionInitializer extends UmbExtensionInitialize
// If the extension has known exports, be sure to run those
if (hasInitExport(moduleInstance)) {
- moduleInstance.onInit(this.host, this.extensionRegistry);
+ await moduleInstance.onInit(this.host, this.extensionRegistry);
}
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/backoffice-entry-point-extension-initializer.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/backoffice-entry-point-extension-initializer.ts
index 29cec8c445..b62e743da6 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/backoffice-entry-point-extension-initializer.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/initializers/backoffice-entry-point-extension-initializer.ts
@@ -32,7 +32,7 @@ export class UmbBackofficeEntryPointExtensionInitializer extends UmbExtensionIni
// If the extension has known exports, be sure to run those
if (hasInitExport(moduleInstance)) {
- moduleInstance.onInit(this.host, this.extensionRegistry);
+ await moduleInstance.onInit(this.host, this.extensionRegistry);
}
}
}
From 4e63a1d974e91d9b7626a28fc18c282c76cd82a3 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 11:38:03 +0200
Subject: [PATCH 31/36] wait for entrypoints to be loaded through the extension
initializer before trying to load them
---
src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 8805c793c8..79cf22f85e 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -17,7 +17,7 @@ import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry,
} from '@umbraco-cms/backoffice/extension-registry';
-import { filter, first } from '@umbraco-cms/backoffice/external/rxjs';
+import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
@@ -118,7 +118,6 @@ export class UmbAppElement extends UmbLitElement {
OpenAPI.BASE = window.location.origin;
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
- new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UUIIconRegistryEssential().attach(this);
@@ -141,6 +140,8 @@ export class UmbAppElement extends UmbLitElement {
// Register public extensions (login extensions)
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions();
+ const initializer = new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
+ await firstValueFrom(initializer.loaded);
// Try to initialise the auth flow and get the runtime status
try {
From 280a149637196d0419f03d1146bfaefc83599baa Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 16:08:10 +0200
Subject: [PATCH 32/36] add localization
---
src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts | 8 ++++----
src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts | 4 +++-
src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 4 +++-
src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 4 +++-
4 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 79cf22f85e..9ec240e899 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -64,16 +64,16 @@ export class UmbAppElement extends UmbLitElement {
setup: (component) => {
const searchParams = new URLSearchParams(window.location.search);
const hasCode = searchParams.has('code');
- (component as UmbAppErrorElement).errorHeadline = 'Umbraco Authorization';
+ (component as UmbAppErrorElement).errorHeadline = this.localize.term('general_login');
(component as UmbAppErrorElement).errorMessage = hasCode
- ? 'Authorization completed. You may now close this window.'
- : 'Authorization failed. Please close this window and try again.';
+ ? this.localize.term('errors_externalLoginSuccess')
+ : this.localize.term('errors_externalLoginFailed');
// Complete the authorization request
this.#authContext?.completeAuthorizationRequest().finally(() => {
// If we don't have an opener, redirect to the root
if (!window.opener) {
- history.replaceState(null, '', '/');
+ //history.replaceState(null, '', '');
}
});
},
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
index a823ed5684..5db74e6d7e 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
@@ -712,7 +712,9 @@ export default {
unauthorized: 'Du har ikke tilladelse til at udføre denne handling',
userNotFound: 'Den angivne bruger blev ikke fundet i databasen',
externalInfoNotFound: 'Serveren kunne ikke kommunikere med den eksterne loginudbyder',
- externalLoginFailed: 'Serveren mislykkedes i at logge ind med den eksterne loginudbyder',
+ externalLoginFailed:
+ 'Serveren mislykkedes i at logge ind med den eksterne loginudbyder. Luk dette vindue og prøv igen.',
+ externalLoginSuccess: 'Du er nu logget ind. Du kan nu lukke dette vindue.',
},
openidErrors: {
accessDenied: 'Access denied',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
index 4a2d45e517..a47a038f95 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
@@ -717,7 +717,9 @@ export default {
unauthorized: 'You were not authorized before performing this action',
userNotFound: 'The local user was not found in the database',
externalInfoNotFound: 'The server did not succeed in communicating with the external login provider',
- externalLoginFailed: 'The server failed to authorize you against the external login provider',
+ externalLoginFailed:
+ 'The server failed to authorize you against the external login provider. Please close the window and try again.',
+ externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
},
openidErrors: {
accessDenied: 'Access denied',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 8a6ec29075..4d5bbcb5c4 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -727,7 +727,9 @@ export default {
unauthorized: 'You were not authorized before performing this action',
userNotFound: 'The local user was not found in the database',
externalInfoNotFound: 'The server did not succeed in communicating with the external login provider',
- externalLoginFailed: 'The server failed to authorize you against the external login provider',
+ externalLoginFailed:
+ 'The server failed to authorize you against the external login provider. Please close the window and try again.',
+ externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
},
openidErrors: {
accessDenied: 'Access denied',
From 7d5fc5d14fad2212a7f36d3a772b59c002a0a201 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Tue, 30 Apr 2024 16:08:57 +0200
Subject: [PATCH 33/36] hide the back button on external login
---
.../src/apps/app/app-error.element.ts | 22 ++++++++++++++-----
.../src/apps/app/app.element.ts | 1 +
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts
index 92027afa18..049e093ddc 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-error.element.ts
@@ -32,6 +32,14 @@ export class UmbAppErrorElement extends UmbLitElement {
@property()
error?: unknown;
+ /**
+ * Hide the back button
+ *
+ * @attr
+ */
+ @property({ type: Boolean, attribute: 'hide-back-button' })
+ hideBackButton = false;
+
constructor() {
super();
@@ -168,11 +176,15 @@ export class UmbAppErrorElement extends UmbLitElement {
- (location.href = '')}>
+ ${this.hideBackButton
+ ? nothing
+ : html`
+ (location.href = '')}>
+ `}
${this.errorHeadline
? this.errorHeadline
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
index 9ec240e899..598f8e2ad6 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts
@@ -64,6 +64,7 @@ export class UmbAppElement extends UmbLitElement {
setup: (component) => {
const searchParams = new URLSearchParams(window.location.search);
const hasCode = searchParams.has('code');
+ (component as UmbAppErrorElement).hideBackButton = true;
(component as UmbAppErrorElement).errorHeadline = this.localize.term('general_login');
(component as UmbAppErrorElement).errorMessage = hasCode
? this.localize.term('errors_externalLoginSuccess')
From ddaebc8c8ab57a04e61cacf4650c55b8a79de227 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Thu, 2 May 2024 11:09:50 +0200
Subject: [PATCH 34/36] send the timeout signal even earlier if UmbAuthFlow
detects that the token has expired or it failed to use the refresh token
---
.../src/packages/core/auth/auth-flow.ts | 17 ++++++++++++-----
.../src/packages/core/auth/auth.context.ts | 7 ++++++-
2 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 89e4f0a1e0..42073f1f9e 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -93,6 +93,7 @@ export class UmbAuthFlow {
readonly #postLogoutRedirectUri: string;
readonly #clientId: string;
readonly #scope: string;
+ readonly #timeoutSignal;
// tokens
#tokenResponse?: TokenResponse;
@@ -101,17 +102,19 @@ export class UmbAuthFlow {
* This signal will emit when the authorization flow is complete.
* @remark It will also emit if there is an error during the authorization flow.
*/
- authorizationSignal = new Subject();
+ readonly authorizationSignal = new Subject();
constructor(
openIdConnectUrl: string,
redirectUri: string,
postLogoutRedirectUri: string,
+ timeoutSignal: Subject,
clientId = 'umbraco-back-office',
scope = 'offline_access',
) {
this.#redirectUri = redirectUri;
this.#postLogoutRedirectUri = postLogoutRedirectUri;
+ this.#timeoutSignal = timeoutSignal;
this.#clientId = clientId;
this.#scope = scope;
@@ -310,7 +313,8 @@ export class UmbAuthFlow {
// if the refresh token is not set (maybe the provider doesn't support them)
if (!this.#tokenResponse?.refreshToken) {
- return Promise.resolve('Missing refreshToken.');
+ this.#timeoutSignal.next();
+ return Promise.reject('Missing refreshToken.');
}
const request = new TokenRequest({
@@ -324,9 +328,12 @@ export class UmbAuthFlow {
await this.#performTokenRequest(request);
- return this.#tokenResponse
- ? Promise.resolve(this.#tokenResponse.accessToken)
- : Promise.reject('Missing accessToken.');
+ if (!this.#tokenResponse) {
+ this.#timeoutSignal.next();
+ return Promise.reject('Missing tokenResponse.');
+ }
+
+ return Promise.resolve(this.#tokenResponse.accessToken);
}
/**
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index 3ce24cd508..f38c2d8853 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -51,7 +51,12 @@ export class UmbAuthContext extends UmbContextBase {
this.#serverUrl = serverUrl;
this.#backofficePath = backofficePath;
- this.#authFlow = new UmbAuthFlow(serverUrl, this.getRedirectUrl(), this.getPostLogoutRedirectUrl());
+ this.#authFlow = new UmbAuthFlow(
+ serverUrl,
+ this.getRedirectUrl(),
+ this.getPostLogoutRedirectUrl(),
+ this.#isTimeout,
+ );
// Observe the authorization signal and close the auth window
this.observe(
From b23e63094f7e226821167658e7af662dd4dd7525 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Thu, 2 May 2024 11:19:08 +0200
Subject: [PATCH 35/36] add extra check to prevent vanilla 'validate' request
to fail on ApiErrors
---
.../context/server-model-validation.context.ts | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts
index 21f0d827fc..6feba69aa1 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validation.context.ts
@@ -60,9 +60,12 @@ export class UmbServerModelValidationContext
if (!this.#isValid) {
// We are missing some typing here, but we will just go wild with 'as any': [NL]
const readErrorBody = (error as any).body;
- Object.keys(readErrorBody.errors).forEach((path) => {
- this.#serverFeedback.push({ path, messages: readErrorBody.errors[path] });
- });
+ // Check if there are validation errors, since the error might be a generic ApiError
+ if (readErrorBody?.errors) {
+ Object.keys(readErrorBody.errors).forEach((path) => {
+ this.#serverFeedback.push({ path, messages: readErrorBody.errors[path] });
+ });
+ }
}
this.#validatePromiseResolve?.();
From adf8aa21c08cffc65a16d58ce2fa40c4598f2c1c Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Thu, 2 May 2024 11:53:09 +0200
Subject: [PATCH 36/36] do not automatically call the `signOut` method since we
now handle it with the timeoutSignal
---
.../src/packages/core/auth/auth-flow.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 42073f1f9e..3d1091854d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -173,8 +173,6 @@ export class UmbAuthFlow {
const response = new TokenResponse(JSON.parse(tokenResponseJson));
if (response.isValid()) {
this.#tokenResponse = response;
- } else {
- this.signOut();
}
}
}
@@ -376,9 +374,8 @@ export class UmbAuthFlow {
try {
this.#tokenResponse = await this.#tokenHandler.performTokenRequest(this.#configuration, request);
} catch (error) {
- // If the token request fails, it means the refresh token is invalid, so we sign the user out.
+ // If the token request fails, it means the refresh token is invalid
console.error('Token request error', error);
- this.signOut();
}
}
}