scrivito
Version:
Scrivito is a professional, yet easy to use SaaS Enterprise Content Management Service, built for digital agencies and medium to large businesses. It is completely maintenance-free, cost-effective, and has unprecedented performance and security.
120 lines (101 loc) • 3.23 kB
text/typescript
import { AuthorizationProvider, RequestFailedError } from 'scrivito_sdk/client';
import { parseErrorResponse } from 'scrivito_sdk/client/parse_error_response';
import {
Verification,
VerificationForChallenge,
Verificator,
fetch as fetchVerificatorFunction,
} from 'scrivito_sdk/client/verificator_functions';
import {
onReset,
promiseAndFinally,
registerAsyncTask,
} from 'scrivito_sdk/common';
export const ERROR_CODE_CLIENT_VERIFICATION_REQUIRED =
'client_verification_required';
export interface Challenge {
verificator: Verificator;
data: unknown;
}
interface CurrentComputation {
challenge: Challenge;
promise: Promise<Verification>;
}
let computation: CurrentComputation | undefined;
let verification: Verification | undefined;
export const PublicAuthentication: AuthorizationProvider = {
async authorize(
request: (authorization: string | undefined) => Promise<Response>
): Promise<Response> {
const response = await request(currentAuthorization());
if (response.status === 401) {
// response.text is a macrotask in firefox.
// it needs to be registered explicitly, to work with flushPromises.
const responseText = await registerAsyncTask(() =>
response.clone().text()
);
const { details, code } = parseErrorResponse(responseText);
if (code === ERROR_CODE_CLIENT_VERIFICATION_REQUIRED) {
if (!isChallenge(details)) {
throw new RequestFailedError('verification without challenge');
}
verification = await computeVerification(details);
return this.authorize(request);
}
}
return response;
},
// integration test support
currentState(): string | null {
const authorization = currentAuthorization();
if (authorization) {
return `Authorization: ${authorization}`;
}
if (computation) {
const challenge = computation.challenge;
return `Pending computation: ${challenge.verificator.id} with ${String(
challenge.data
)}`;
}
return null;
},
};
async function computeVerification(
challenge: Challenge
): Promise<Verification> {
if (!computation) {
// note that further request's challenges are ignored (intentionally)
const { verificator, data } = challenge;
const promise = (async () => {
const compute: VerificationForChallenge = await fetchVerificatorFunction(
verificator.id,
verificator.url
);
return new Promise<Verification>((resolve) => {
compute(data, (result: Verification) => resolve(result));
});
})();
computation = {
challenge: { verificator, data },
promise: promiseAndFinally<Verification>(promise, () => {
computation = undefined;
}),
};
}
return computation.promise;
}
function isChallenge(maybeChallenge: object): maybeChallenge is Challenge {
return !!(maybeChallenge as Challenge).verificator;
}
function currentAuthorization(): string | undefined {
if (!verification) return;
if (verification.expiresAfter < new Date()) {
verification = undefined;
return;
}
return verification.authorization;
}
onReset(() => {
computation = undefined;
verification = undefined;
});