Skip to content

Commit 8f2b6d5

Browse files
authored
Merge branch 'main' into community-and-support-welcome-dashboard-content
2 parents 0b553f9 + b04739e commit 8f2b6d5

20 files changed

+300
-160
lines changed

docs/authentication.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ There are two ways to use this:
3636
"CMS": {
3737
"Security":{
3838
"BackOfficeHost": "http://localhost:5173",
39-
"AuthorizeCallbackPathName": "/"
39+
"AuthorizeCallbackPathName": "/oauth_complete",
40+
"AuthorizeCallbackLogoutPathName": "/logout",
41+
"AuthorizeCallbackErrorPathName": "/error",
4042
},
4143
},
4244
[...]

src/apps/app/app-auth.controller.ts

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
import {
2-
UMB_AUTH_CONTEXT,
3-
UMB_MODAL_APP_AUTH,
4-
UMB_STORAGE_REDIRECT_URL,
5-
type UmbUserLoginState,
6-
} from '@umbraco-cms/backoffice/auth';
1+
import { UMB_AUTH_CONTEXT, UMB_MODAL_APP_AUTH, type UmbUserLoginState } from '@umbraco-cms/backoffice/auth';
72
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
83
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
9-
import { filter, firstValueFrom, skip } from '@umbraco-cms/backoffice/external/rxjs';
4+
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
105
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
116
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
12-
import type { ManifestAuthProvider } from '@umbraco-cms/backoffice/extension-registry';
137

148
export class UmbAppAuthController extends UmbControllerBase {
159
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
16-
#firstTimeLoggingIn = true;
1710

1811
constructor(host: UmbControllerHost) {
1912
super(host);
@@ -23,14 +16,8 @@ export class UmbAppAuthController extends UmbControllerBase {
2316

2417
// Observe the user's authorization state and start the authorization flow if the user is not authorized
2518
this.observe(
26-
context.isAuthorized.pipe(
27-
// Skip the first since it is always false
28-
skip(1),
29-
// Only continue if the value is false
30-
filter((x) => !x),
31-
),
19+
context.timeoutSignal,
3220
() => {
33-
this.#firstTimeLoggingIn = false;
3421
this.makeAuthorizationRequest('timedOut');
3522
},
3623
'_authState',
@@ -66,11 +53,6 @@ export class UmbAppAuthController extends UmbControllerBase {
6653
throw new Error('[Fatal] Auth context is not available');
6754
}
6855

69-
// Save location.href so we can redirect to it after login
70-
if (location.href !== this.#authContext.getPostLogoutRedirectUrl()) {
71-
window.sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, location.href);
72-
}
73-
7456
// Figure out which providers are available
7557
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry));
7658

@@ -80,7 +62,7 @@ export class UmbAppAuthController extends UmbControllerBase {
8062

8163
// If the user is timed out, we can show the login modal directly
8264
if (userLoginState === 'timedOut') {
83-
const selected = await this.#showLoginModal(userLoginState, availableProviders);
65+
const selected = await this.#showLoginModal(userLoginState);
8466

8567
if (!selected) {
8668
return false;
@@ -91,7 +73,7 @@ export class UmbAppAuthController extends UmbControllerBase {
9173

9274
if (availableProviders.length === 1) {
9375
// One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider
94-
this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName);
76+
await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName, true);
9577
return this.#updateState();
9678
}
9779

@@ -103,12 +85,12 @@ export class UmbAppAuthController extends UmbControllerBase {
10385

10486
if (redirectProvider) {
10587
// Redirect directly to the provider
106-
this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName);
88+
await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName, true);
10789
return this.#updateState();
10890
}
10991

11092
// Show the provider selection screen
111-
const selected = await this.#showLoginModal(userLoginState, availableProviders);
93+
const selected = await this.#showLoginModal(userLoginState);
11294

11395
if (!selected) {
11496
return false;
@@ -117,45 +99,32 @@ export class UmbAppAuthController extends UmbControllerBase {
11799
return this.#updateState();
118100
}
119101

120-
async #showLoginModal(
121-
userLoginState: UmbUserLoginState,
122-
availableProviders: Array<ManifestAuthProvider>,
123-
): Promise<boolean> {
102+
async #showLoginModal(userLoginState: UmbUserLoginState): Promise<boolean> {
124103
if (!this.#authContext) {
125104
throw new Error('[Fatal] Auth context is not available');
126105
}
127106

128-
// Check if any provider denies local login
129-
const denyLocalLogin = availableProviders.some((provider) => provider.meta?.behavior?.denyLocalLogin);
130-
if (denyLocalLogin) {
131-
// Unregister the Umbraco provider
132-
umbExtensionsRegistry.unregister('Umb.AuthProviders.Umbraco');
133-
}
134-
135107
// Show the provider selection screen
108+
const authModalKey = 'umbAuthModal';
136109
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
137-
modalManager.remove('umbAuthModal');
110+
138111
const selected = await modalManager
139112
.open(this._host, UMB_MODAL_APP_AUTH, {
140113
data: {
141114
userLoginState,
142115
},
143116
modal: {
144-
key: 'umbAuthModal',
145-
backdropBackground: this.#firstTimeLoggingIn
146-
? 'var(--umb-auth-backdrop, rgb(244, 244, 244))'
147-
: 'var(--umb-auth-backdrop-timedout, rgba(244, 244, 244, 0.75))',
117+
key: authModalKey,
118+
backdropBackground: 'var(--umb-auth-backdrop, rgb(244, 244, 244))',
148119
},
149120
})
150121
.onSubmit()
151122
.catch(() => undefined);
152123

153-
if (!selected?.providerName) {
124+
if (!selected?.success) {
154125
return false;
155126
}
156127

157-
this.#authContext.makeAuthorizationRequest(selected.providerName, selected.loginHint);
158-
159128
return true;
160129
}
161130

@@ -164,9 +133,6 @@ export class UmbAppAuthController extends UmbControllerBase {
164133
throw new Error('[Fatal] Auth context is not available');
165134
}
166135

167-
// Reinitialize the auth flow (load the state from local storage)
168-
this.#authContext.setInitialState();
169-
170136
// The authorization flow is finished, so let the caller know if the user is authorized
171137
return this.#authContext.getIsAuthorized();
172138
}

src/apps/app/app-error.element.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export class UmbAppErrorElement extends UmbLitElement {
3232
@property()
3333
error?: unknown;
3434

35+
/**
36+
* Hide the back button
37+
*
38+
* @attr
39+
*/
40+
@property({ type: Boolean, attribute: 'hide-back-button' })
41+
hideBackButton = false;
42+
3543
constructor() {
3644
super();
3745

@@ -168,11 +176,15 @@ export class UmbAppErrorElement extends UmbLitElement {
168176
169177
<div id="container" class="uui-text">
170178
<uui-box id="box" headline-variant="h1">
171-
<uui-button
172-
slot="header-actions"
173-
label=${this.localize.term('general_back')}
174-
look="secondary"
175-
@click=${() => (location.href = '')}></uui-button>
179+
${this.hideBackButton
180+
? nothing
181+
: html`
182+
<uui-button
183+
slot="header-actions"
184+
label=${this.localize.term('general_back')}
185+
look="secondary"
186+
@click=${() => (location.href = '')}></uui-button>
187+
`}
176188
<div slot="headline">
177189
${this.errorHeadline
178190
? this.errorHeadline

src/apps/app/app.element.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
UmbAppEntryPointExtensionInitializer,
1818
umbExtensionsRegistry,
1919
} from '@umbraco-cms/backoffice/extension-registry';
20+
import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
2021

2122
@customElement('umb-app')
2223
export class UmbAppElement extends UmbLitElement {
@@ -57,6 +58,27 @@ export class UmbAppElement extends UmbLitElement {
5758
path: 'install',
5859
component: () => import('../installer/installer.element.js'),
5960
},
61+
{
62+
path: 'oauth_complete',
63+
component: () => import('./app-error.element.js'),
64+
setup: (component) => {
65+
const searchParams = new URLSearchParams(window.location.search);
66+
const hasCode = searchParams.has('code');
67+
(component as UmbAppErrorElement).hideBackButton = true;
68+
(component as UmbAppErrorElement).errorHeadline = this.localize.term('general_login');
69+
(component as UmbAppErrorElement).errorMessage = hasCode
70+
? this.localize.term('errors_externalLoginSuccess')
71+
: this.localize.term('errors_externalLoginFailed');
72+
73+
// Complete the authorization request
74+
this.#authContext?.completeAuthorizationRequest().finally(() => {
75+
// If we don't have an opener, redirect to the root
76+
if (!window.opener) {
77+
history.replaceState(null, '', '');
78+
}
79+
});
80+
},
81+
},
6082
{
6183
path: 'upgrade',
6284
component: () => import('../upgrader/upgrader.element.js'),
@@ -67,6 +89,17 @@ export class UmbAppElement extends UmbLitElement {
6789
resolve: () => {
6890
this.#authContext?.clearTokenStorage();
6991
this.#authController.makeAuthorizationRequest('loggedOut');
92+
93+
// Listen for the user to be authorized
94+
this.#authContext?.isAuthorized
95+
.pipe(
96+
filter((x) => !!x),
97+
first(),
98+
)
99+
.subscribe(() => {
100+
// Redirect to the root
101+
history.replaceState(null, '', '');
102+
});
70103
},
71104
},
72105
{
@@ -86,7 +119,6 @@ export class UmbAppElement extends UmbLitElement {
86119
OpenAPI.BASE = window.location.origin;
87120

88121
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
89-
new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
90122

91123
new UUIIconRegistryEssential().attach(this);
92124

@@ -109,6 +141,8 @@ export class UmbAppElement extends UmbLitElement {
109141

110142
// Register public extensions (login extensions)
111143
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions();
144+
const initializer = new UmbAppEntryPointExtensionInitializer(this, umbExtensionsRegistry);
145+
await firstValueFrom(initializer.loaded);
112146

113147
// Try to initialise the auth flow and get the runtime status
114148
try {
@@ -161,12 +195,12 @@ export class UmbAppElement extends UmbLitElement {
161195
}
162196

163197
#redirect() {
164-
// If there is a ?code parameter in the url, then we are in the middle of the oauth flow
165-
// and we need to complete the login (the authorization notifier will redirect after this is done
166-
// essentially hitting this method again)
167-
const queryParams = new URLSearchParams(window.location.search);
168-
if (queryParams.has('code')) {
169-
this.#authContext?.completeAuthorizationRequest();
198+
const pathname = pathWithoutBasePath({ start: true, end: false });
199+
200+
// If we are on the oauth_complete or error page, we should not redirect
201+
if (pathname === '/oauth_complete' || pathname === '/error') {
202+
// Initialize the router
203+
history.replaceState(null, '', location.href);
170204
return;
171205
}
172206

@@ -184,8 +218,6 @@ export class UmbAppElement extends UmbLitElement {
184218
break;
185219

186220
case RuntimeLevelModel.RUN: {
187-
const pathname = pathWithoutBasePath({ start: true, end: false });
188-
189221
// If we are on installer or upgrade page, redirect to the root since we are in the RUN state
190222
if (pathname === '/install' || pathname === '/upgrade') {
191223
history.replaceState(null, '', '/');

src/assets/lang/da-dk.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,9 @@ export default {
712712
unauthorized: 'Du har ikke tilladelse til at udføre denne handling',
713713
userNotFound: 'Den angivne bruger blev ikke fundet i databasen',
714714
externalInfoNotFound: 'Serveren kunne ikke kommunikere med den eksterne loginudbyder',
715-
externalLoginFailed: 'Serveren mislykkedes i at logge ind med den eksterne loginudbyder',
715+
externalLoginFailed:
716+
'Serveren mislykkedes i at logge ind med den eksterne loginudbyder. Luk dette vindue og prøv igen.',
717+
externalLoginSuccess: 'Du er nu logget ind. Du kan nu lukke dette vindue.',
716718
},
717719
openidErrors: {
718720
accessDenied: 'Access denied',

src/assets/lang/en-us.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,9 @@ export default {
717717
unauthorized: 'You were not authorized before performing this action',
718718
userNotFound: 'The local user was not found in the database',
719719
externalInfoNotFound: 'The server did not succeed in communicating with the external login provider',
720-
externalLoginFailed: 'The server failed to authorize you against the external login provider',
720+
externalLoginFailed:
721+
'The server failed to authorize you against the external login provider. Please close the window and try again.',
722+
externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
721723
},
722724
openidErrors: {
723725
accessDenied: 'Access denied',

src/assets/lang/en.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,9 @@ export default {
727727
unauthorized: 'You were not authorized before performing this action',
728728
userNotFound: 'The local user was not found in the database',
729729
externalInfoNotFound: 'The server did not succeed in communicating with the external login provider',
730-
externalLoginFailed: 'The server failed to authorize you against the external login provider',
730+
externalLoginFailed:
731+
'The server failed to authorize you against the external login provider. Please close the window and try again.',
732+
externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
731733
},
732734
openidErrors: {
733735
accessDenied: 'Access denied',

src/external/openid/redirect_based_handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
6969
this.storageBackend.setItem(authorizationServiceConfigurationKey(handle), JSON.stringify(configuration.toJson())),
7070
]);
7171

72-
persisted.then(() => {
72+
return persisted.then(() => {
7373
// make the redirect request
7474
const url = this.buildRequestUrl(configuration, request);
7575
log('Making a request to ', request, url);
76-
this.locationLike.assign(url);
76+
return url;
7777
});
7878
}
7979

src/external/rxjs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ export {
1818
filter,
1919
startWith,
2020
skip,
21+
first,
2122
} from 'rxjs';

src/libs/extension-api/initializers/extension-initializer-base.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
44
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
55
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
66
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
7+
import { ReplaySubject } from '@umbraco-cms/backoffice/external/rxjs';
78

89
/**
910
* Base class for extension initializers, which are responsible for loading and unloading extensions.
@@ -15,24 +16,30 @@ export abstract class UmbExtensionInitializerBase<
1516
protected host;
1617
protected extensionRegistry;
1718
#extensionMap = new Map();
19+
#loaded = new ReplaySubject<void>(1);
20+
loaded = this.#loaded.asObservable();
1821

1922
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<T>, manifestType: Key) {
2023
super(host);
2124
this.host = host;
2225
this.extensionRegistry = extensionRegistry;
23-
this.observe(extensionRegistry.byType<Key, T>(manifestType), (extensions) => {
26+
this.observe(extensionRegistry.byType<Key, T>(manifestType), async (extensions) => {
2427
this.#extensionMap.forEach((existingExt) => {
2528
if (!extensions.find((b) => b.alias === existingExt.alias)) {
2629
this.unloadExtension(existingExt);
2730
this.#extensionMap.delete(existingExt.alias);
2831
}
2932
});
3033

31-
extensions.forEach((extension) => {
32-
if (this.#extensionMap.has(extension.alias)) return;
33-
this.#extensionMap.set(extension.alias, extension);
34-
this.instantiateExtension(extension);
35-
});
34+
await Promise.all(
35+
extensions.map((extension) => {
36+
if (this.#extensionMap.has(extension.alias)) return;
37+
this.#extensionMap.set(extension.alias, extension);
38+
return this.instantiateExtension(extension);
39+
}),
40+
);
41+
42+
this.#loaded.next();
3643
});
3744
}
3845

0 commit comments

Comments
 (0)