#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.'); } };