233 lines
9.2 KiB
JavaScript
233 lines
9.2 KiB
JavaScript
#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, detail) => {
|
|
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', detail === undefined ? undefined : `Requested scope "${detail.scope}" is ${detail.reason}.`);
|
|
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, resolved);
|
|
|
|
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, resolved);
|
|
|
|
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;
|
|
}
|
|
|
|
eAuthsInfo.replaceWith(new HanyuuOAuth2AppInfo(resolved.app).element);
|
|
eAuthsScope.replaceWith(new HanyuuOAuth2AppScopeList(resolved.scope).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.');
|
|
}
|
|
};
|