hanyuu/assets/oauth2.js/authorise.js

237 lines
9.2 KiB
JavaScript
Raw Normal View History

2024-07-30 21:24:20 +00:00
#include loading.jsx
#include app/info.jsx
#include app/scope.jsx
#include header/header.js
#include header/user.jsx
const HanyuuOAuth2AuthoriseErrors = Object.freeze({
'invalid_request': {
description: 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
},
'unauthorized_client': {
description: 'The client is not authorised to request an authorisation code using this method.',
},
'access_denied': {
description: 'The resource owner or authorization server denied the request.',
},
'unsupported_response_type': {
description: 'The authorisation server does not support obtaining an authorisation code using this method.',
},
'invalid_scope': {
description: 'The requested scope is invalid, unknown, or malformed.',
},
'server_error': {
description: 'The authorisation server encountered an unexpected condition that prevented it from fulfilling the request.',
},
'temporarily_unavailable': {
description: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
},
});
const HanyuuOAuth2Authorise = async () => {
const queryParams = new URLSearchParams(window.location.search);
const loading = new HanyuuOAuth2Loading('.js-loading');
const header = new HanyuuOAuth2Header;
const fAuths = document.querySelector('.js-authorise-form');
const eAuthsInfo = document.querySelector('.js-authorise-form-info');
const eAuthsScope = document.querySelector('.js-authorise-form-scope');
const dError = document.querySelector('.js-authorise-error');
const dErrorText = dError?.querySelector('.js-authorise-error-text');
let scope;
let state;
let redirectUri;
let redirectUriRaw;
const displayError = (error, description, documentation) => {
if(redirectUri === undefined) {
if(error in HanyuuOAuth2AuthoriseErrors) {
const errInfo = HanyuuOAuth2AuthoriseErrors[error];
description ??= errInfo.description;
} else
description = `An unknown error occurred: ${error}`;
dErrorText.textContent = description;
header.setSimpleData('error', 'An error occurred!');
header.removeElement();
loading.visible = false;
fAuths.classList.add('hidden');
dError.classList.remove('hidden');
return;
}
const errorUri = new URL(redirectUri);
errorUri.searchParams.set('error', error?.toString() ?? 'invalid_request');
if(description)
errorUri.searchParams.set('error_description', description.toString());
if(documentation)
errorUri.searchParams.set('error_uri', documentation.toString());
if(state !== undefined)
errorUri.searchParams.set('state', state.toString());
window.location.assign(errorUri);
};
const translateError = serverError => {
if(serverError === 'auth')
return displayError('access_denied');
if(serverError === 'csrf')
return displayError('invalid_request', 'Request verification failed.');
if(serverError === 'client')
return displayError('invalid_request', 'There is no application associated with the specified Client ID.');
if(serverError === 'format')
return displayError('invalid_request', 'Redirect URI specified is not registered with this application.');
if(serverError === 'method')
return displayError('invalid_request', 'Requested code challenge method is not supported.');
if(serverError === 'length')
return displayError('invalid_request', 'Code challenge length is not acceptable.');
if(serverError === 'required')
return displayError('invalid_request', 'A registered redirect URI must be specified.');
if(serverError === 'scope')
return displayError('invalid_scope');
if(serverError === 'authorise')
return displayError('server_error', 'Server was unable to complete authorisation.');
return displayError('invalid_request', `An unknown error occurred: ${serverError}.`);
};
if(queryParams.has('redirect_uri'))
try {
const qRedirectUriRaw = queryParams.get('redirect_uri');
const qRedirectUri = new URL(qRedirectUriRaw);
if(qRedirectUri.protocol !== 'https:')
throw 'protocol must be https';
redirectUri = qRedirectUri;
redirectUriRaw = qRedirectUriRaw;
} catch(ex) {
return displayError('invalid_request', 'Invalid redirect URI specified.');
}
if(queryParams.has('state')) {
const qState = queryParams.get('state');
if(qState.length > 1000)
return displayError('invalid_request', 'State parameter may not be longer than 255 characters.');
state = qState;
}
if(queryParams.get('response_type') !== 'code')
return displayError('unsupported_response_type');
let codeChallengeMethod = 'plain';
if(queryParams.has('code_challenge_method')) {
codeChallengeMethod = queryParams.get('code_challenge_method');
if(!['plain', 'S256'].includes(codeChallengeMethod))
return translateError('method');
}
if(!queryParams.has('code_challenge'))
return displayError('invalid_request', 'code_challenge must be specified.');
const codeChallenge = queryParams.get('code_challenge');
if(codeChallengeMethod === 'S256') {
if(codeChallenge.length !== 43)
return displayError('invalid_request', 'Specified code challenge is not a valid SHA-256 hash.');
} else {
if(codeChallenge.length < 43)
return displayError('invalid_request', 'Code challenge must be at least 43 characters long.');
if(codeChallenge.length > 128)
return displayError('invalid_request', 'Code challenge may not be longer than 128 characters.');
}
if(!queryParams.has('client_id'))
return displayError('invalid_request', 'client_id must be specified.');
const resolveParams = new URLSearchParams;
resolveParams.set('csrfp', HanyuuCSRFP.getToken());
resolveParams.set('client', queryParams.get('client_id'));
if(redirectUriRaw !== undefined)
resolveParams.set('redirect', redirectUriRaw);
if(queryParams.has('scope')) {
scope = queryParams.get('scope');
resolveParams.set('scope', scope);
}
try {
const resolved = (await $x.get(`/oauth2/resolve-authorise-app?${resolveParams}`, { type: 'json' })).body();
if(!resolved)
throw 'authorisation resolve failed';
if(typeof resolved.error === 'string')
return translateError(resolved.error);
const userHeader = new HanyuuOAuth2UserHeader(resolved.user);
header.setElement(userHeader);
const verifyAuthsRequest = async () => {
const params = {
_csrfp: HanyuuCSRFP.getToken(),
client: queryParams.get('client_id'),
cc: codeChallenge,
ccm: codeChallengeMethod,
};
if(redirectUriRaw !== undefined)
params.redirect = redirectUriRaw;
if(scope !== undefined)
params.scope = scope;
try {
const response = (await $x.post('/oauth2/authorise', { type: 'json' }, params)).body();
if(!response)
throw 'authorisation failed';
if(typeof response.error === 'string')
return translateError(response.error);
const authoriseUri = new URL(response.redirect);
authoriseUri.searchParams.set('code', response.code);
if(state !== undefined)
authoriseUri.searchParams.set('state', state.toString());
window.location.assign(authoriseUri);
} catch(ex) {
console.error(ex);
translateError('authorise');
}
};
if(resolved.app.trusted && resolved.user.guise === undefined) {
if(userHeader)
userHeader.guiseVisible = false;
verifyAuthsRequest();
return;
}
const appElem = new HanyuuOAuth2AppInfo(resolved.app);
eAuthsInfo.replaceWith(appElem.element);
const scopeElem = new HanyuuOAuth2AppScopeList();
eAuthsScope.replaceWith(scopeElem.element);
fAuths.onsubmit = ev => {
ev.preventDefault();
loading.visible = true;
fAuths.classList.add('hidden');
if(userHeader)
userHeader.guiseVisible = false;
if(ev.submitter?.value === 'yes')
verifyAuthsRequest();
else
displayError('access_denied');
};
loading.visible = false;
fAuths.classList.remove('hidden');
} catch(ex) {
console.error(ex);
displayError('server_error', 'Server was unable to respond to the client info request.');
}
};