diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts index 1858bb966d..7bf9c1fa48 100644 --- a/src/Umbraco.Web.UI.Login/src/auth.element.ts +++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts @@ -60,8 +60,6 @@ const createForm = (elements: HTMLElement[]) => { @customElement('umb-auth') export default class UmbAuthElement extends LitElement { - #returnPath = ''; - /** * Disables the local login form and only allows external login providers. * @@ -89,12 +87,7 @@ export default class UmbAuthElement extends LitElement { @property({ type: String, attribute: 'return-url' }) set returnPath(value: string) { - this.#returnPath = value; - umbAuthContext.returnPath = this.returnPath; - } - get returnPath() { - // Check if there is a ?redir querystring or else return the returnUrl attribute - return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath; + umbAuthContext.returnPath = value; } /** diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.context.ts b/src/Umbraco.Web.UI.Login/src/context/auth.context.ts index 4abd9441a3..b84ecfd039 100644 --- a/src/Umbraco.Web.UI.Login/src/context/auth.context.ts +++ b/src/Umbraco.Web.UI.Login/src/context/auth.context.ts @@ -15,7 +15,30 @@ export class UmbAuthContext implements IUmbAuthContext { #authRepository = new UmbAuthRepository(); - public returnPath = ''; + #returnPath = ''; + + set returnPath(value: string) { + this.#returnPath = value; + } + + /** + * Gets the return path from the query string. + * + * It will first look for a `ReturnUrl` parameter, then a `returnPath` parameter, and finally the `returnPath` property. + * + * @returns The return path from the query string. + */ + get returnPath(): string { + const params = new URLSearchParams(window.location.search); + let returnUrl = params.get('ReturnUrl') ?? params.get('returnPath') ?? this.#returnPath; + + // Paths from the old Backoffice are encoded twice and need to be decoded, + // but we don't want to decode the new paths coming from the Management API. + if (returnUrl.indexOf('/security/back-office/authorize') === -1) { + returnUrl = decodeURIComponent(returnUrl); + } + return returnUrl || ''; + } async login(data: LoginRequestModel): Promise { return this.#authRepository.login(data); diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts index 46e0ca4215..75ab8bb496 100644 --- a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts +++ b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts @@ -10,254 +10,289 @@ import { umbLocalizationContext } from '../external/localization/localization-co export class UmbAuthRepository { readonly #authURL = 'backoffice/umbracoapi/authentication/postlogin'; - public async login(data: LoginRequestModel): Promise { - try { - const request = new Request(this.#authURL, { - method: 'POST', - body: JSON.stringify({ - username: data.username, - password: data.password, - rememberMe: data.persist, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + public async login(data: LoginRequestModel): Promise { + try { + const request = new Request(this.#authURL, { + method: 'POST', + body: JSON.stringify({ + username: data.username, + password: data.password, + rememberMe: data.persist, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - const text = await response.text(); - const responseData = JSON.parse(this.#removeAngularJSResponseData(text)); + const responseData: LoginResponse = { + status: response.status + }; - return { - status: response.status, - error: response.ok ? undefined : await this.#getErrorText(response), - data: responseData, - twoFactorView: responseData?.twoFactorView, - }; - } catch (error) { - return { - status: 500, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - } + if (!response.ok) { + responseData.error = await this.#getErrorText(response); + return responseData; + } - public async resetPassword(email: string): Promise { - const request = new Request('backoffice/umbracoapi/authentication/PostRequestPasswordReset', { - method: 'POST', - body: JSON.stringify({ - email, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + // Additionally authenticate with the Management API + await this.#managementApiLogin(data.username, data.password); - return { - status: response.status, - error: response.ok ? undefined : await this.#getErrorText(response), - }; - } + try { + const text = await response.text(); + if (text) { + responseData.data = JSON.parse(this.#removeAngularJSResponseData(text)); + } + } catch {} - public async validatePasswordResetCode(user: string, code: string): Promise { - const request = new Request('backoffice/umbracoapi/authentication/validatepasswordresetcode', { - method: 'POST', - body: JSON.stringify({ - userId: user, - resetCode: code, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + return { + status: response.status, + data: responseData?.data, + twoFactorView: responseData?.twoFactorView, + }; + } catch (error) { + return { + status: 500, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } - return { - status: response.status, - error: response.ok ? undefined : await this.#getErrorText(response), - }; - } + public async resetPassword(email: string): Promise { + const request = new Request('backoffice/umbracoapi/authentication/PostRequestPasswordReset', { + method: 'POST', + body: JSON.stringify({ + email, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async newPassword(password: string, resetCode: string, userId: number): Promise { - const request = new Request('backoffice/umbracoapi/authentication/PostSetPassword', { - method: 'POST', - body: JSON.stringify({ - password, - resetCode, - userId, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + return { + status: response.status, + error: response.ok ? undefined : await this.#getErrorText(response), + }; + } - return { - status: response.status, - error: response.ok ? undefined : await this.#getErrorText(response), - }; - } + public async validatePasswordResetCode(user: string, code: string): Promise { + const request = new Request('backoffice/umbracoapi/authentication/validatepasswordresetcode', { + method: 'POST', + body: JSON.stringify({ + userId: user, + resetCode: code, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async newInvitedUserPassword(newPassWord: string): Promise { - const request = new Request('backoffice/umbracoapi/authentication/PostSetInvitedUserPassword', { - method: 'POST', - body: JSON.stringify({ - newPassWord, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + return { + status: response.status, + error: response.ok ? undefined : await this.#getErrorText(response), + }; + } - return { - status: response.status, - error: response.ok ? undefined : await this.#getErrorText(response), - }; - } + public async newPassword(password: string, resetCode: string, userId: number): Promise { + const request = new Request('backoffice/umbracoapi/authentication/PostSetPassword', { + method: 'POST', + body: JSON.stringify({ + password, + resetCode, + userId, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async getPasswordConfig(userId: string): Promise { - //TODO: Add type - const request = new Request(`backoffice/umbracoapi/authentication/GetPasswordConfig?userId=${userId}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + return { + status: response.status, + error: response.ok ? undefined : await this.#getErrorText(response), + }; + } - // Check if response contains AngularJS response data - if (response.ok) { - let text = await response.text(); - text = this.#removeAngularJSResponseData(text); - const data = JSON.parse(text); + public async newInvitedUserPassword(newPassWord: string): Promise { + const request = new Request('backoffice/umbracoapi/authentication/PostSetInvitedUserPassword', { + method: 'POST', + body: JSON.stringify({ + newPassWord, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - return { - status: response.status, - data, - }; - } + return { + status: response.status, + error: response.ok ? undefined : await this.#getErrorText(response), + }; + } - return { - status: response.status, - error: response.ok ? undefined : this.#getErrorText(response), - }; - } + public async getPasswordConfig(userId: string): Promise { + //TODO: Add type + const request = new Request(`backoffice/umbracoapi/authentication/GetPasswordConfig?userId=${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async getInvitedUser(): Promise { - //TODO: Add type - const request = new Request('backoffice/umbracoapi/authentication/GetCurrentInvitedUser', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + // Check if response contains AngularJS response data + if (response.ok) { + let text = await response.text(); + text = this.#removeAngularJSResponseData(text); + const data = JSON.parse(text); - // Check if response contains AngularJS response data - if (response.ok) { - let text = await response.text(); - text = this.#removeAngularJSResponseData(text); - const user = JSON.parse(text); + return { + status: response.status, + data, + }; + } - return { - status: response.status, - user, - }; - } + return { + status: response.status, + error: response.ok ? undefined : this.#getErrorText(response), + }; + } - return { - status: response.status, - error: this.#getErrorText(response), - }; - } + public async getInvitedUser(): Promise { + //TODO: Add type + const request = new Request('backoffice/umbracoapi/authentication/GetCurrentInvitedUser', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async getMfaProviders(): Promise { - const request = new Request('backoffice/umbracoapi/authentication/Get2faProviders', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - const response = await fetch(request); + // Check if response contains AngularJS response data + if (response.ok) { + let text = await response.text(); + text = this.#removeAngularJSResponseData(text); + const user = JSON.parse(text); - // Check if response contains AngularJS response data - if (response.ok) { - let text = await response.text(); - text = this.#removeAngularJSResponseData(text); - const providers = JSON.parse(text); + return { + status: response.status, + user, + }; + } - return { - status: response.status, - providers, - }; - } + return { + status: response.status, + error: this.#getErrorText(response), + }; + } - return { - status: response.status, - error: await this.#getErrorText(response), - providers: [], - }; - } + public async getMfaProviders(): Promise { + const request = new Request('backoffice/umbracoapi/authentication/Get2faProviders', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const response = await fetch(request); - public async validateMfaCode(code: string, provider: string): Promise { - const request = new Request('backoffice/umbracoapi/authentication/PostVerify2faCode', { - method: 'POST', - body: JSON.stringify({ - code, - provider, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); + // Check if response contains AngularJS response data + if (response.ok) { + let text = await response.text(); + text = this.#removeAngularJSResponseData(text); + const providers = JSON.parse(text); - const response = await fetch(request); + return { + status: response.status, + providers, + }; + } + + return { + status: response.status, + error: await this.#getErrorText(response), + providers: [], + }; + } + + public async validateMfaCode(code: string, provider: string): Promise { + const request = new Request('backoffice/umbracoapi/authentication/PostVerify2faCode', { + method: 'POST', + body: JSON.stringify({ + code, + provider, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + const response = await fetch(request); let text = await response.text(); text = this.#removeAngularJSResponseData(text); const data = JSON.parse(text); - if (response.ok) { - return { + if (response.ok) { + return { data, - status: response.status, - }; - } + status: response.status, + }; + } - return { - status: response.status, - error: data.Message ?? 'An unknown error occurred.', - }; - } + return { + status: response.status, + error: data.Message ?? 'An unknown error occurred.', + }; + } - async #getErrorText(response: Response): Promise { - switch (response.status) { - case 400: - case 401: - return umbLocalizationContext.localize('login_userFailedLogin', undefined, "Oops! We couldn't log you in. Please check your credentials and try again."); + async #getErrorText(response: Response): Promise { + switch (response.status) { + case 400: + case 401: + return umbLocalizationContext.localize('login_userFailedLogin', undefined, "Oops! We couldn't log you in. Please check your credentials and try again."); - case 402: - return umbLocalizationContext.localize('login_2faText', undefined, 'You have enabled 2-factor authentication and must verify your identity.'); + case 402: + return umbLocalizationContext.localize('login_2faText', undefined, 'You have enabled 2-factor authentication and must verify your identity.'); - case 500: - return umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server'); + case 500: + return umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server'); - default: - return response.statusText ?? await umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server') - } - } + default: + return response.statusText ?? await umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server') + } + } - /** - * AngularJS adds a prefix to the response data, which we need to remove - */ - #removeAngularJSResponseData(text: string) { - if (text.startsWith(")]}',\n")) { - text = text.split('\n')[1]; - } + /** + * AngularJS adds a prefix to the response data, which we need to remove + */ + #removeAngularJSResponseData(text: string) { + if (text.startsWith(")]}',\n")) { + text = text.split('\n')[1]; + } - return text; - } + return text; + } + + async #managementApiLogin(username: string, password: string) { + try { + const authURLManagementApi = 'management/api/v1/security/back-office/login'; + const requestManagementApi = new Request(authURLManagementApi, { + method: 'POST', + body: JSON.stringify({ + username, + password, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + return await fetch(requestManagementApi); + } catch (error) { + console.error('Failed to authenticate with the Management API:', error); + } + } }