V14: make v13 login screen work initially with Management API (#15170)

* make the login check a bit more robust to be able to handle both old postlogin and new management api response types

* v14 only: update the login url to work with the management api (this will still work with the old backoffice to some extent)

* Revert "v14 only: update the login url to work with the management api (this will still work with the old backoffice to some extent)"

This reverts commit 0639ca80f0ce620b3555b959d5ff10678730acfd.

* V14 only: additionally authenticate with the Management API to set the right cookies
This commit is contained in:
Jacob Overgaard
2023-11-10 13:38:08 +01:00
parent 31678bb676
commit 81caf2b384
3 changed files with 272 additions and 221 deletions

View File

@@ -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;
}
/**

View File

@@ -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<LoginResponse> {
return this.#authRepository.login(data);

View File

@@ -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<LoginResponse> {
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<LoginResponse> {
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<ResetPasswordResponse> {
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<ValidatePasswordResetCodeResponse> {
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<ResetPasswordResponse> {
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<LoginResponse> {
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<ValidatePasswordResetCodeResponse> {
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<LoginResponse> {
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<LoginResponse> {
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<any> {
//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<LoginResponse> {
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<any> {
//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<any> {
//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<any> {
//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<MfaProvidersResponse> {
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<MfaProvidersResponse> {
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<LoginResponse> {
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<LoginResponse> {
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<string> {
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<string> {
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);
}
}
}