diff --git a/assets/oauth2.css/device.css b/assets/oauth2.css/device.css index 2e653fa..0747d7d 100644 --- a/assets/oauth2.css/device.css +++ b/assets/oauth2.css/device.css @@ -1,22 +1,3 @@ -.oauth2-devicehead { - display: flex; - align-items: center; -} - -.oauth2-devicehead-icon { - flex: 0 0 auto; - background-color: #fff; - mask: url('/images/mobile-screen-solid.svg') no-repeat center; - width: 40px; - height: 40px; - margin: 10px; -} - -.oauth2-devicehead-text { - font-size: 1.8em; - line-height: 1.4em; -} - .oauth2-device-form { display: flex; justify-content: center; diff --git a/assets/oauth2.css/error.css b/assets/oauth2.css/error.css index 2668cf1..41f3b68 100644 --- a/assets/oauth2.css/error.css +++ b/assets/oauth2.css/error.css @@ -1,22 +1,3 @@ -.oauth2-errorhead { - display: flex; - align-items: center; -} - -.oauth2-errorhead-icon { - flex: 0 0 auto; - background-color: #fff; - mask: url('/images/circle-exclamation-solid.svg') no-repeat center; - width: 40px; - height: 40px; - margin: 10px; -} - -.oauth2-errorhead-text { - font-size: 1.8em; - line-height: 1.4em; -} - .oauth2-errorbody p { margin: .5em 1em; } diff --git a/assets/oauth2.css/login.css b/assets/oauth2.css/login.css deleted file mode 100644 index 2dd595e..0000000 --- a/assets/oauth2.css/login.css +++ /dev/null @@ -1,18 +0,0 @@ -.oauth2-loginhead { - display: flex; - align-items: center; -} - -.oauth2-loginhead-icon { - flex: 0 0 auto; - background-color: #fff; - mask: url('/images/user-lock-solid.svg') no-repeat center; - width: 40px; - height: 40px; - margin: 10px; -} - -.oauth2-loginhead-text { - font-size: 1.8em; - line-height: 1.4em; -} diff --git a/assets/oauth2.css/main.css b/assets/oauth2.css/main.css index 690ecbd..8b5ec41 100644 --- a/assets/oauth2.css/main.css +++ b/assets/oauth2.css/main.css @@ -38,8 +38,8 @@ @include loading.css; @include banner.css; @include error.css; -@include login.css; @include device.css; +@include simplehead.css; @include userhead.css; @include appinfo.css; @include scope.css; diff --git a/assets/oauth2.css/simplehead.css b/assets/oauth2.css/simplehead.css new file mode 100644 index 0000000..639c1da --- /dev/null +++ b/assets/oauth2.css/simplehead.css @@ -0,0 +1,30 @@ +.oauth2-simplehead { + display: flex; + align-items: center; +} + +.oauth2-simplehead-icon { + flex: 0 0 auto; + background-color: #fff; + mask: url('/images/circle-question-solid.svg') no-repeat center; + width: 40px; + height: 40px; + margin: 10px; +} +.oauth2-simplehead-icon--code { + mask-image: url('/images/mobile-screen-solid.svg'); +} +.oauth2-simplehead-icon--error { + mask-image: url('/images/circle-exclamation-solid.svg'); +} +.oauth2-simplehead-icon--login { + mask-image: url('/images/user-lock-solid.svg'); +} +.oauth2-simplehead-icon--wait { + mask-image: url('/images/ellipsis-solid.svg'); +} + +.oauth2-simplehead-text { + font-size: 1.8em; + line-height: 1.4em; +} diff --git a/assets/oauth2.js/authorise.js b/assets/oauth2.js/authorise.js new file mode 100644 index 0000000..3374981 --- /dev/null +++ b/assets/oauth2.js/authorise.js @@ -0,0 +1,236 @@ +#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.'); + } +}; diff --git a/assets/oauth2.js/header/header.js b/assets/oauth2.js/header/header.js index 4161293..7f925d9 100644 --- a/assets/oauth2.js/header/header.js +++ b/assets/oauth2.js/header/header.js @@ -15,11 +15,13 @@ const HanyuuOAuth2Header = function(element = '.js-oauth2-header', simpleElement if(hasSimpleElement) simpleElement.classList.toggle('hidden', !state); }; - const setSimpleData = (name, text) => { + const setSimpleData = (icon, text) => { if(hasSimpleElement) { - simpleElement.className = `oauth2-${name}`; - simpleElementIcon.className = `oauth2-${name}-icon`; - simpleElementText.className = `oauth2-${name}-text`; + for(const className of simpleElementIcon.classList) + if(className.startsWith('oauth2-simplehead-icon--')) + simpleElementIcon.classList.remove(className); + + simpleElementIcon.classList.add(`oauth2-simplehead-icon--${icon}`); simpleElementText.textContent = text; } }; diff --git a/assets/oauth2.js/main.js b/assets/oauth2.js/main.js index 182ba17..29b8204 100644 --- a/assets/oauth2.js/main.js +++ b/assets/oauth2.js/main.js @@ -1,37 +1,9 @@ +#include authorise.js #include verify.js (() => { - const authoriseButtons = document.querySelectorAll('.js-authorise-action'); - - for(const button of authoriseButtons) { - button.disabled = false; - - button.onclick = () => { - for(const other of authoriseButtons) - other.disabled = true; - - const body = []; - for(const name in button.dataset) - body.push(encodeURIComponent(name) + '=' + encodeURIComponent(button.dataset[name])); - - const xhr = new XMLHttpRequest; - xhr.responseType = 'json'; - xhr.open('POST', '/oauth2/authorise'); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onload = () => { - if(xhr.response.redirect) - location.assign(xhr.response.redirect); - else - location.assign('/oauth2/error?error=invalid_request'); - }; - xhr.onerror = () => { - for(const other of authoriseButtons) - other.disabled = false; - }; - xhr.send(body.join('&')); - }; - } - + if(location.pathname === '/oauth2/authorise') + HanyuuOAuth2Authorise(); if(location.pathname === '/oauth2/verify') HanyuuOAuth2Verify(); })(); diff --git a/assets/oauth2.js/verify.js b/assets/oauth2.js/verify.js index 32bd210..38363fb 100644 --- a/assets/oauth2.js/verify.js +++ b/assets/oauth2.js/verify.js @@ -18,49 +18,48 @@ const HanyuuOAuth2Verify = () => { let userCode = ''; let userHeader; + const verifyAuthsRequest = async approve => { - return await $x.post('/oauth2/verify', { type: 'json' }, { - _csrfp: HanyuuCSRFP.getToken(), - code: userCode, - approve: approve === true ? 'yes' : 'no', - }); - }; + try { + const response = (await $x.post('/oauth2/verify', { type: 'json' }, { + _csrfp: HanyuuCSRFP.getToken(), + code: userCode, + approve: approve === true ? 'yes' : 'no', + })).body(); - const handleVerifyAuthsResponse = result => { - const response = result.body(); + if(!response) + throw 'response is empty'; - if(!response) { + if(typeof response.error === 'string') { + // TODO: nicer errors + if(response.error === 'auth') + alert('You are not logged in.'); + else if(response.error === 'csrf') + alert('Request verification failed, please refresh and try again.'); + else if(response.error === 'code') + alert('This code is not associated with any authorisation request.'); + else if(response.error === 'approval') + alert('The authorisation request associated with this code is not pending approval.'); + else if(response.error === 'invalid') + alert('Invalid approval state specified.'); + else + alert(`An unknown error occurred: ${response.error}`); + + loading.visible = false; + fAuths.classList.remove('hidden'); + return; + } + + loading.visible = false; + if(response.approval === 'approved') + rApproved.classList.remove('hidden'); + else + rDenied.classList.remove('hidden'); + } catch(ex) { alert('Request to verify endpoint failed. Please try again.'); loading.visible = false; fAuths.classList.remove('hidden'); - return; } - - if(typeof response.error === 'string') { - // TODO: nicer errors - if(response.error === 'auth') - alert('You are not logged in.'); - else if(response.error === 'csrf') - alert('Request verification failed, please refresh and try again.'); - else if(response.error === 'code') - alert('This code is not associated with any authorisation request.'); - else if(response.error === 'approval') - alert('The authorisation request associated with this code is not pending approval.'); - else if(response.error === 'invalid') - alert('Invalid approval state specified.'); - else - alert(`An unknown error occurred: ${response.error}`); - - loading.visible = false; - fAuths.classList.remove('hidden'); - return; - } - - loading.visible = false; - if(response.approval === 'approved') - rApproved.classList.remove('hidden'); - else - rDenied.classList.remove('hidden'); }; fAuths.onsubmit = ev => { @@ -72,8 +71,7 @@ const HanyuuOAuth2Verify = () => { if(userHeader) userHeader.guiseVisible = false; - verifyAuthsRequest(ev.submitter.value === 'yes') - .then(handleVerifyAuthsResponse); + verifyAuthsRequest(ev.submitter.value === 'yes'); }; const fCode = document.querySelector('.js-verify-code'); @@ -85,7 +83,7 @@ const HanyuuOAuth2Verify = () => { loading.visible = true; fCode.classList.add('hidden'); - $x.get(`/oauth2/resolve-request?csrfp=${encodeURIComponent(HanyuuCSRFP.getToken())}&code=${encodeURIComponent(eUserCode.value)}`, { type: 'json' }) + $x.get(`/oauth2/resolve-verify?csrfp=${encodeURIComponent(HanyuuCSRFP.getToken())}&code=${encodeURIComponent(eUserCode.value)}`, { type: 'json' }) .then(result => { const response = result.body(); @@ -123,7 +121,7 @@ const HanyuuOAuth2Verify = () => { if(userHeader) userHeader.guiseVisible = false; - verifyAuthsRequest(true).then(handleVerifyAuthsResponse); + verifyAuthsRequest(true); return; } @@ -135,6 +133,10 @@ const HanyuuOAuth2Verify = () => { loading.visible = false; fAuths.classList.remove('hidden'); + }).catch(() => { + alert('Request to resolve endpoint failed. Please try again.'); + loading.visible = false; + fCode.classList.remove('hidden'); }); }; diff --git a/database/2024_07_30_211859_remove_state_fields_from_authorisations_db.php b/database/2024_07_30_211859_remove_state_fields_from_authorisations_db.php new file mode 100644 index 0000000..9aa02da --- /dev/null +++ b/database/2024_07_30_211859_remove_state_fields_from_authorisations_db.php @@ -0,0 +1,9 @@ +execute('ALTER TABLE hau_oauth2_authorise DROP COLUMN auth_state, DROP COLUMN auth_approval;'); + } +} diff --git a/public/images/circle-question-solid.svg b/public/images/circle-question-solid.svg new file mode 100644 index 0000000..3cc83fd --- /dev/null +++ b/public/images/circle-question-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/ellipsis-solid.svg b/public/images/ellipsis-solid.svg new file mode 100644 index 0000000..7283ce3 --- /dev/null +++ b/public/images/ellipsis-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/OAuth2/OAuth2AuthoriseData.php b/src/OAuth2/OAuth2AuthorisationData.php similarity index 52% rename from src/OAuth2/OAuth2AuthoriseData.php rename to src/OAuth2/OAuth2AuthorisationData.php index b24c726..df30d03 100644 --- a/src/OAuth2/OAuth2AuthoriseData.php +++ b/src/OAuth2/OAuth2AuthorisationData.php @@ -9,7 +9,7 @@ use Index\Data\IDbConnection; use Hanyuu\Apps\AppInfo; use Hanyuu\Apps\AppUriInfo; -class OAuth2AuthoriseData { +class OAuth2AuthorisationData { private IDbConnection $dbConn; private DbStatementCache $cache; @@ -18,26 +18,21 @@ class OAuth2AuthoriseData { $this->cache = new DbStatementCache($dbConn); } - public function getAuthoriseInfo( - ?string $authoriseId = null, + public function getAuthorisationInfo( + ?string $authsId = null, AppInfo|string|null $appInfo = null, - ?string $userId = null, ?string $code = null - ): OAuth2AuthoriseInfo { + ): OAuth2AuthorisationInfo { $selectors = []; $values = []; - if($authoriseId !== null) { + if($authsId !== null) { $selectors[] = 'auth_id = ?'; - $values[] = $authoriseId; + $values[] = $authsId; } if($appInfo !== null) { $selectors[] = 'app_id = ?'; $values[] = $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo; } - if($userId !== null) { - $selectors[] = 'user_id = ?'; - $values[] = $userId; - } if($code !== null) { $selectors[] = 'auth_code = ?'; $values[] = $code; @@ -47,7 +42,7 @@ class OAuth2AuthoriseData { throw new RuntimeException('Insufficient data to do authorisation request lookup.'); $args = 0; - $stmt = $this->cache->get('SELECT auth_id, app_id, user_id, uri_id, auth_state, auth_challenge_code, auth_challenge_method, auth_scope, auth_code, auth_approval, UNIX_TIMESTAMP(auth_created), UNIX_TIMESTAMP(auth_expires) FROM hau_oauth2_authorise WHERE ' . implode(' AND ', $selectors)); + $stmt = $this->cache->get('SELECT auth_id, app_id, user_id, uri_id, auth_challenge_code, auth_challenge_method, auth_scope, auth_code, UNIX_TIMESTAMP(auth_created), UNIX_TIMESTAMP(auth_expires) FROM hau_oauth2_authorise WHERE ' . implode(' AND ', $selectors)); foreach($values as $value) $stmt->addParameter(++$args, $value); $stmt->execute(); @@ -56,45 +51,41 @@ class OAuth2AuthoriseData { if(!$result->next()) throw new RuntimeException('Authorise request not found.'); - return OAuth2AuthoriseInfo::fromResult($result); + return OAuth2AuthorisationInfo::fromResult($result); } - public function createAuthorise( + public function createAuthorisation( AppInfo|string $appInfo, string $userId, AppUriInfo|string $appUriInfo, - string $state, string $challengeCode, string $challengeMethod, string $scope, - ?string $code = null, - bool $preapprove = false - ): OAuth2AuthoriseInfo { + ?string $code = null + ): OAuth2AuthorisationInfo { $code ??= XString::random(60); - $stmt = $this->cache->get('INSERT INTO hau_oauth2_authorise (app_id, user_id, uri_id, auth_state, auth_challenge_code, auth_challenge_method, auth_scope, auth_code, auth_approval) VALUES (?, ?, ?, ?, ?, ?, ?, ?, IF(?, "approved", DEFAULT(auth_approval)))'); + $stmt = $this->cache->get('INSERT INTO hau_oauth2_authorise (app_id, user_id, uri_id, auth_challenge_code, auth_challenge_method, auth_scope, auth_code) VALUES (?, ?, ?, ?, ?, ?, ?)'); $stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo); $stmt->addParameter(2, $userId); $stmt->addParameter(3, $appUriInfo instanceof AppUriInfo ? $appUriInfo->getId() : $appUriInfo); - $stmt->addParameter(4, $state); - $stmt->addParameter(5, $challengeCode); - $stmt->addParameter(6, $challengeMethod); - $stmt->addParameter(7, $scope); - $stmt->addParameter(8, $code); - $stmt->addParameter(9, $preapprove ? 1 : 0); + $stmt->addParameter(4, $challengeCode); + $stmt->addParameter(5, $challengeMethod); + $stmt->addParameter(6, $scope); + $stmt->addParameter(7, $code); $stmt->execute(); - return $this->getAuthoriseInfo(authoriseId: (string)$this->dbConn->getLastInsertId()); + return $this->getAuthorisationInfo(authsId: (string)$this->dbConn->getLastInsertId()); } - public function deleteAuthorise( - OAuth2AuthoriseInfo|string|null $authoriseInfo = null + public function deleteAuthorisation( + OAuth2AuthorisationInfo|string|null $authsInfo = null ): void { $selectors = []; $values = []; - if($authoriseInfo !== null) { + if($authsInfo !== null) { $selectors[] = 'auth_id = ?'; - $values[] = $authoriseInfo instanceof OAuth2AuthoriseInfo ? $authoriseInfo->getId() : $authoriseInfo; + $values[] = $authsInfo instanceof OAuth2AuthorisationInfo ? $authsInfo->getId() : $authsInfo; } $query = 'DELETE FROM hau_oauth2_authorise'; @@ -107,18 +98,4 @@ class OAuth2AuthoriseData { $stmt->addParameter(++$args, $value); $stmt->execute(); } - - public function setAuthoriseApproval(OAuth2AuthoriseInfo|string $authoriseInfo, bool $approval): void { - if($authoriseInfo instanceof OAuth2AuthoriseInfo) { - if(!$authoriseInfo->isPending()) - return; - - $authoriseInfo = $authoriseInfo->getId(); - } - - $stmt = $this->cache->get('UPDATE hau_oauth2_authorise SET auth_approval = ? WHERE auth_id = ? AND auth_approval = "pending"'); - $stmt->addParameter(1, $approval ? 'approved' : 'denied'); - $stmt->addParameter(2, $authoriseInfo); - $stmt->execute(); - } } diff --git a/src/OAuth2/OAuth2AuthoriseInfo.php b/src/OAuth2/OAuth2AuthorisationInfo.php similarity index 69% rename from src/OAuth2/OAuth2AuthoriseInfo.php rename to src/OAuth2/OAuth2AuthorisationInfo.php index 77c7644..5906e70 100644 --- a/src/OAuth2/OAuth2AuthoriseInfo.php +++ b/src/OAuth2/OAuth2AuthorisationInfo.php @@ -4,36 +4,32 @@ namespace Hanyuu\OAuth2; use Index\Data\IDbResult; use Index\Serialisation\UriBase64; -class OAuth2AuthoriseInfo { +class OAuth2AuthorisationInfo { public function __construct( private string $id, private string $appId, private string $userId, private string $uriId, - private string $state, private string $challengeCode, private string $challengeMethod, private string $scope, private string $code, - private string $approval, private int $created, private int $expires ) {} - public static function fromResult(IDbResult $result): OAuth2AuthoriseInfo { - return new OAuth2AuthoriseInfo( + public static function fromResult(IDbResult $result): OAuth2AuthorisationInfo { + return new OAuth2AuthorisationInfo( id: $result->getString(0), appId: $result->getString(1), userId: $result->getString(2), uriId: $result->getString(3), - state: $result->getString(4), - challengeCode: $result->getString(5), - challengeMethod: $result->getString(6), - scope: $result->getString(7), - code: $result->getString(8), - approval: $result->getString(9), - created: $result->getInteger(10), - expires: $result->getInteger(11), + challengeCode: $result->getString(4), + challengeMethod: $result->getString(5), + scope: $result->getString(6), + code: $result->getString(7), + created: $result->getInteger(8), + expires: $result->getInteger(9), ); } @@ -53,10 +49,6 @@ class OAuth2AuthoriseInfo { return $this->userId; } - public function getState(): string { - return $this->state; - } - public function getChallengeCode(): string { return $this->challengeCode; } @@ -89,19 +81,6 @@ class OAuth2AuthoriseInfo { return $this->code; } - public function getApproval(): string { - return $this->approval; - } - public function isPending(): bool { - return strcasecmp($this->approval, 'pending') === 0; - } - public function isApproved(): bool { - return strcasecmp($this->approval, 'approved') === 0; - } - public function isDenied(): bool { - return strcasecmp($this->approval, 'denied') === 0; - } - public function getCreatedTime(): int { return $this->created; } diff --git a/src/OAuth2/OAuth2Context.php b/src/OAuth2/OAuth2Context.php index b4d7fb5..965719c 100644 --- a/src/OAuth2/OAuth2Context.php +++ b/src/OAuth2/OAuth2Context.php @@ -5,18 +5,18 @@ use Index\Data\IDbConnection; use Hanyuu\Apps\AppInfo; class OAuth2Context { - private OAuth2AuthoriseData $authorise; + private OAuth2AuthorisationData $authorisations; private OAuth2TokensData $tokens; private OAuth2DevicesData $devices; public function __construct(IDbConnection $dbConn) { - $this->authorise = new OAuth2AuthoriseData($dbConn); + $this->authorisations = new OAuth2AuthorisationData($dbConn); $this->tokens = new OAuth2TokensData($dbConn); $this->devices = new OAuth2DevicesData($dbConn); } - public function getAuthoriseData(): OAuth2AuthoriseData { - return $this->authorise; + public function getAuthorisationData(): OAuth2AuthorisationData { + return $this->authorisations; } public function getTokensData(): OAuth2TokensData { diff --git a/src/OAuth2/OAuth2Routes.php b/src/OAuth2/OAuth2Routes.php index 2c723f1..cd4ca06 100644 --- a/src/OAuth2/OAuth2Routes.php +++ b/src/OAuth2/OAuth2Routes.php @@ -33,15 +33,6 @@ final class OAuth2Routes extends RouteHandler { return $info; } - private static function buildCallbackUri(string $target, array $params): string { - if($target === '') - $target = '/oauth2/error'; - - $target .= strpos($target, '?') === false ? '?' : '&'; - $target .= http_build_query($params, '', '&', PHP_QUERY_RFC3986); - return $target; - } - private static function filterScopes(string $source): array { $scopes = []; @@ -57,205 +48,16 @@ final class OAuth2Routes extends RouteHandler { return $scopes; } - private const AUTHORISE_ERRORS = [ - '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.', - 'allow_description' => true, - ], - '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' => [ - 'status' => 500, - 'description' => 'The authorisation server encountered an unexpected condition that prevented it from fulfilling the request.', - ], - 'temporarily_unavailable' => [ - 'status' => 503, - 'description' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.', - ], - ]; - - #[HttpGet('/oauth2/error')] - public function getError($response, $request) { - $error = (string)$request->getParam('error'); - if(!array_key_exists($error, self::AUTHORISE_ERRORS)) - return 404; - - $info = self::AUTHORISE_ERRORS[$error]; - - $description = $info['description']; - if($request->hasParam('error_description') && array_key_exists('allow_description', $info) && $info['allow_description']) - $description = $request->getParam('error_description'); - - $statusCode = 400; - if(array_key_exists('status', $info)) - $statusCode = $info['status']; - - $response->setStatusCode($statusCode); - - return $this->templating->render('oauth2/error', [ - 'error_code' => $error, - 'error_description' => $description, - ]); - } - #[HttpGet('/oauth2/authorise')] public function getAuthorise($response, $request) { - $redirectUri = (string)$request->getParam('redirect_uri'); - if($redirectUri !== '' && filter_var($redirectUri, FILTER_VALIDATE_URL) === false) - return $response->redirect(self::buildCallbackUri('', [ - 'error' => 'invalid_request', - 'error_description' => 'Request URI must be a correctly formatted absolute URI.', - ])); - - $clientId = (string)$request->getParam('client_id'); - $appsData = $this->appsCtx->getData(); - try { - $appInfo = $appsData->getAppInfo(clientId: $clientId, deleted: false); - } catch(RuntimeException $ex) { - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Client ID is missing or is not associated with an existing application.', - ])); - } - - $state = (string)$request->getParam('state'); - if(strlen($state) > 255) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'State parameter may not be longer than 255 characters.', - ])); - - if($redirectUri === '') { - $uriInfos = $appsData->countAppUris($appInfo); - if($uriInfos !== 1) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'A registered redirect URI must be specified.', - 'state' => $state, - ])); - - $uriInfos = $appsData->getAppUriInfos($appInfo); - if(count($uriInfos) !== 1) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'A registered redirect URI must be specified (congrats, you hit an edge case!).', - 'state' => $state, - ])); - - $uriInfo = array_pop($uriInfos); - $redirectUriId = $uriInfo->getId(); - $redirectUri = $uriInfo->getString(); - } else { - $redirectUriId = $appsData->getAppUriId($appInfo, $redirectUri); - if($redirectUriId === null) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Specified redirect URI is not registered with the application.', - 'state' => $state, - ])); - } - - $type = (string)$request->getParam('response_type'); - if($type !== 'code') - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'unsupported_response_type', - 'state' => $state, - ])); - - $codeChallengeMethod = (string)$request->getParam('code_challenge_method'); - if($codeChallengeMethod === '') $codeChallengeMethod = 'plain'; - - $codeChallenge = (string)$request->getParam('code_challenge'); - $codeChallengeLength = strlen($codeChallenge); - - if($codeChallengeMethod === 'plain') { - if($codeChallengeLength < 43) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Code challenge must be at least 43 characters long.', - 'state' => $state, - ])); - elseif($codeChallengeLength > 128) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Code challenge may not be longer than 128 characters.', - 'state' => $state, - ])); - } elseif($codeChallengeMethod === 'S256') { - if($codeChallengeLength !== 43) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Specified code challenge is not a valid SHA-256 hash.', - 'state' => $state, - ])); - } else { - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Request code challenge method is not supported.', - 'state' => $state, - ])); - } - - $scopes = self::filterScopes((string)$request->getParam('scope')); - if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_scope', - 'state' => $state, - ])); - $scope = implode(' ', $scopes); - $authInfo = ($this->getAuthInfo)(); if(!isset($authInfo->user)) - return $this->templating->render('oauth2/login', [ - 'app' => $appInfo, - 'auth' => $authInfo, - ]); - - $authoriseData = $this->oauth2Ctx->getAuthoriseData(); - try { - $authoriseInfo = $authoriseData->createAuthorise( - $appInfo, - $authInfo->user->id, - $redirectUriId, - $state, - $codeChallenge, - $codeChallengeMethod, - $scope, - preapprove: $appInfo->isTrusted() - ); - } catch(RuntimeException $ex) { - return $response->redirect(self::buildCallbackUri($redirectUri, [ - 'error' => 'server_error', - 'state' => $state, - ])); - } - - if($authoriseInfo->isApproved()) { - $response->redirect(self::buildCallbackUri($redirectUri, [ - 'code' => $authoriseInfo->getCode(), - 'state' => $authoriseInfo->getState(), - ])); - return; - } + return $this->templating->render('oauth2/login', ['auth' => $authInfo]); $csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token); return $this->templating->render('oauth2/authorise', [ - 'app' => $appInfo, - 'req' => $authoriseInfo, - 'auth' => $authInfo, 'csrfp_token' => $csrfp->createToken(), - 'redirect_uri' => $redirectUri, ]); } @@ -264,127 +66,155 @@ final class OAuth2Routes extends RouteHandler { if(!$request->isFormContent()) return 400; - $content = $request->getContent(); - - $redirectUri = (string)$content->getParam('redirect'); - if(filter_var($redirectUri, FILTER_VALIDATE_URL) === false) { - $response->setStatusCode(400); - return [ - 'redirect' => self::buildCallbackUri('', [ - 'error' => 'invalid_request', - ]), - ]; - } + // TODO: RATE LIMITING $authInfo = ($this->getAuthInfo)(); - if(!isset($authInfo->user)) { - $response->setStatusCode(403); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'access_denied', - ]), - ]; - } + if(!isset($authInfo->user)) + return ['error' => 'auth']; + + $content = $request->getContent(); $csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token); - if(!$csrfp->verifyToken((string)$content->getParam('csrfp'))) { - $response->setStatusCode(403); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Request verification failed.', - ]), - ]; + if(!$csrfp->verifyToken((string)$content->getParam('_csrfp'))) + return ['error' => 'csrf']; + + $codeChallengeMethod = 'plain'; + if($content->hasParam('ccm')) { + $codeChallengeMethod = $content->getParam('ccm'); + if(!in_array($codeChallengeMethod, ['plain', 'S256'])) + return ['error' => 'method']; } - $approve = (string)$content->getParam('approve'); - if(!in_array($approve, ['yes', 'no'])) { - $response->setStatusCode(400); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - ]), - ]; - } - - $authoriseData = $this->oauth2Ctx->getAuthoriseData(); - try { - $authoriseInfo = $authoriseData->getAuthoriseInfo( - userId: $authInfo->user->id, - code: (string)$content->getParam('code'), - ); - } catch(RuntimeException $ex) { - $response->setStatusCode(404); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Could not find authorisation request.', - ]), - ]; - } - - if(!$authoriseInfo->isPending()) { - $response->setStatusCode(410); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'This authorisation request has already been handled.', - 'state' => $authoriseInfo->getState(), - ]), - ]; + $codeChallenge = $content->getParam('cc'); + $codeChallengeLength = strlen($codeChallenge); + if($codeChallengeMethod === 'S256') { + if($codeChallengeLength !== 43) + return ['error' => 'length']; + } else { + if($codeChallengeLength < 43 || $codeChallengeLength > 128) + return ['error' => 'length']; } $appsData = $this->appsCtx->getData(); try { - $uriInfo = $appsData->getAppUriInfo($authoriseInfo->getUriId()); + $appInfo = $appsData->getAppInfo(clientId: (string)$content->getParam('client'), deleted: false); } catch(RuntimeException $ex) { - $response->setStatusCode(400); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'This authorisation request was made with a redirect URI that is no longer registered with this application.', - 'state' => $authoriseInfo->getState(), - ]), - ]; + return ['error' => 'client']; } - if($uriInfo->getString() !== $redirectUri) { - $response->setStatusCode(400); - return [ - 'redirect' => self::buildCallbackUri($redirectUri, [ - 'error' => 'invalid_request', - 'error_description' => 'Attempt at request forgery detected.', - 'state' => $authoriseInfo->getState(), - ]), - ]; + $scope = ''; + if($content->hasParam('scope')) { + $scopes = self::filterScopes((string)$content->getParam('scope')); + if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) + return ['error' => 'scope']; + + $scope = implode(' ', $scopes); } - $approved = $approve === 'yes'; - $authoriseData->setAuthoriseApproval($authoriseInfo, $approved); + if($content->hasParam('redirect')) { + $redirectUri = (string)$content->getParam('redirect'); + $redirectUriId = $appsData->getAppUriId($appInfo, $redirectUri); + if($redirectUriId === null) + return ['error' => 'format']; + } else { + $uriInfos = $appsData->getAppUriInfos($appInfo); + if(count($uriInfos) !== 1) + return ['error' => 'required']; - if($approved) - return [ - 'redirect' => self::buildCallbackUri($uriInfo->getString(), [ - 'code' => $authoriseInfo->getCode(), - 'state' => $authoriseInfo->getState(), - ]), - ]; + $uriInfo = array_pop($uriInfos); + $redirectUri = $uriInfo->getString(); + $redirectUriId = $uriInfo->getId(); + } + + $authsData = $this->oauth2Ctx->getAuthorisationData(); + try { + $authsInfo = $authsData->createAuthorisation( + $appInfo, + $authInfo->user->id, + $redirectUriId, + $codeChallenge, + $codeChallengeMethod, + $scope + ); + } catch(RuntimeException $ex) { + return ['error' => 'authorise', 'detail' => $ex->getMessage()]; + } return [ - 'redirect' => self::buildCallbackUri($uriInfo->getString(), [ - 'error' => 'access_denied', - 'state' => $authoriseInfo->getState(), - ]), + 'code' => $authsInfo->getCode(), + 'redirect' => $redirectUri, ]; } + #[HttpGet('/oauth2/resolve-authorise-app')] + public function getResolveAuthorise($response, $request) { + // TODO: RATE LIMITING + + $authInfo = ($this->getAuthInfo)(); + if(!isset($authInfo->user)) + return ['error' => 'auth']; + + $csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token); + if(!$csrfp->verifyToken((string)$request->getParam('csrfp'))) + return ['error' => 'csrf']; + + $appsData = $this->appsCtx->getData(); + try { + $appInfo = $appsData->getAppInfo(clientId: (string)$request->getParam('client'), deleted: false); + } catch(RuntimeException $ex) { + return ['error' => 'client']; + } + + if($request->hasParam('redirect')) { + $redirectUri = (string)$request->getParam('redirect'); + if($appsData->getAppUriId($appInfo, $redirectUri) === null) + return ['error' => 'format']; + } else { + $uriInfos = $appsData->getAppUriInfos($appInfo); + if(count($uriInfos) !== 1) + return ['error' => 'required']; + } + + if($request->hasParam('scope')) { + $scopes = self::filterScopes((string)$request->getParam('scope')); + if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) + return ['error' => 'scope']; + } + + $result = [ + 'app' => [ + 'name' => $appInfo->getName(), + 'summary' => $appInfo->getSummary(), + 'trusted' => $appInfo->isTrusted(), + 'links' => [ + ['title' => 'Website', 'display' => $appInfo->getWebsiteDisplay(), 'uri' => $appInfo->getWebsite()], + ], + ], + 'user' => [ + 'name' => $authInfo->user->name, + 'colour' => $authInfo->user->colour, + 'profile_uri' => $authInfo->user->profile_url, + 'avatar_uri' => $authInfo->user->avatars->x120, + ], + ]; + + if(isset($authInfo->guise)) + $result['user']['guise'] = [ + 'name' => $authInfo->guise->name, + 'colour' => $authInfo->guise->colour, + 'profile_uri' => $authInfo->guise->profile_url, + 'revert_uri' => $authInfo->guise->revert_url, + 'avatar_uri' => $authInfo->guise->avatars->x60, + ]; + + return $result; + } + #[HttpGet('/oauth2/verify')] public function getVerify($response, $request) { $authInfo = ($this->getAuthInfo)(); if(!isset($authInfo->user)) - return $this->templating->render('oauth2/login', [ - 'auth' => $authInfo, - ]); + return $this->templating->render('oauth2/login', ['auth' => $authInfo]); $csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token); @@ -432,8 +262,8 @@ final class OAuth2Routes extends RouteHandler { ]; } - #[HttpGet('/oauth2/resolve-request')] - public function getResolveRequest($response, $request) { + #[HttpGet('/oauth2/resolve-verify')] + public function getResolveVerify($response, $request) { // TODO: RATE LIMITING $authInfo = ($this->getAuthInfo)(); @@ -652,9 +482,9 @@ final class OAuth2Routes extends RouteHandler { $type = (string)$content->getParam('grant_type'); if($type === 'authorization_code') { - $authoriseData = $this->oauth2Ctx->getAuthoriseData(); + $authsData = $this->oauth2Ctx->getAuthorisationData(); try { - $authoriseInfo = $authoriseData->getAuthoriseInfo( + $authsInfo = $authsData->getAuthorisationInfo( appInfo: $appInfo, code: (string)$content->getParam('code'), ); @@ -663,24 +493,19 @@ final class OAuth2Routes extends RouteHandler { return self::error('invalid_grant', 'No authorisation request with this code exists.'); } - if($authoriseInfo->hasExpired()) { + if($authsInfo->hasExpired()) { $response->setStatusCode(400); return self::error('invalid_grant', 'Authorisation request has expired.'); } - if(!$authoriseInfo->verifyCodeChallenge((string)$content->getParam('code_verifier'))) { + if(!$authsInfo->verifyCodeChallenge((string)$content->getParam('code_verifier'))) { $response->setStatusCode(400); return self::error('invalid_request', 'Code challenge verification failed.'); } - if(!$authoriseInfo->isApproved()) { - $response->setStatusCode(400); - return self::error('invalid_grant', 'Authorisation request has not been approved.'); - } + $authsData->deleteAuthorisation($authsInfo); - $authoriseData->deleteAuthorise($authoriseInfo); - - $scopes = $authoriseInfo->getScopes(); + $scopes = $authsInfo->getScopes(); if(!$this->oauth2Ctx->validateScopes($appInfo, $scopes)) { $response->setStatusCode(400); return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.'); @@ -690,12 +515,12 @@ final class OAuth2Routes extends RouteHandler { $tokensData = $this->oauth2Ctx->getTokensData(); $accessInfo = $tokensData->createAccess( $appInfo, - $authoriseInfo->getUserId(), + $authsInfo->getUserId(), scope: $scope, ); // 'scope' only has to be in the response if it differs from what was requested - if($scope === $authoriseInfo->getScope()) + if($scope === $authsInfo->getScope()) unset($scope); if($appInfo->shouldIssueRefreshToken()) diff --git a/templates/oauth2/authorise.twig b/templates/oauth2/authorise.twig index abadc5d..a1391f1 100644 --- a/templates/oauth2/authorise.twig +++ b/templates/oauth2/authorise.twig @@ -1,81 +1,29 @@ {% extends 'oauth2/master.twig' %} +{% set body_header_icon = 'wait' %} +{% set body_header_text = 'Loading...' %} {% set body_title = 'Authorisation Request' %} -{% block body_header %} -
-
-
-
-
- -
-
- -
- {% if auth.guise is defined %} -
-
-
- -
-
-
-

Are you {{ auth.guise.name }} and did you mean to use your own account?

-

Click here and reload this page.

-
-
- {% endif %} -
-
-{% endblock %} - {% block body_content %} -
-

A third-party application is requesting permission to access your account.

-
+
-
-
- {{ app.name }} -
{# TODO: author should be listed #} - -
-

{{ app.summary }}

+ -
-
This application will be able to:
-
-
-
-
Do anything because I have not made up scopes yet.
-
-
-
-
Eat soup.
-
-
-
-
These are placeholders.
-
-
-
-
This one is really long because I want to test wrapping and how the chevron icon thing will handle it so there will be a lot of text here, the app will not be gaining anything from it but yeah sometimes you just need to explode seventy times.
-
+
-
- - -
+
+
+ +
+ + +
+ {% endblock %} diff --git a/templates/oauth2/error.twig b/templates/oauth2/error.twig deleted file mode 100644 index faebf56..0000000 --- a/templates/oauth2/error.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'oauth2/master.twig' %} - -{% set body_header_class = 'errorhead' %} -{% set body_header_text = 'Error' %} -{% set body_title = 'An error occurred' %} - -{% block body_content %} -
-

{{ error_description }}

-
-{% endblock %} diff --git a/templates/oauth2/login.twig b/templates/oauth2/login.twig index c2f4f52..04da046 100644 --- a/templates/oauth2/login.twig +++ b/templates/oauth2/login.twig @@ -1,6 +1,6 @@ {% extends 'oauth2/master.twig' %} -{% set body_header_class = 'loginhead' %} +{% set body_header_icon = 'login' %} {% set body_header_text = 'Not logged in' %} {% set body_title = 'Authorisation Request' %} diff --git a/templates/oauth2/master.twig b/templates/oauth2/master.twig index b410712..d2335a2 100644 --- a/templates/oauth2/master.twig +++ b/templates/oauth2/master.twig @@ -17,9 +17,10 @@ {% block body %} {% block body_header %}
-
-
-
+ {% set body_header_icon = body_header_icon|default('') %} +
+
+
{{ body_header_text|default('') }}
diff --git a/templates/oauth2/verify.twig b/templates/oauth2/verify.twig index fe5bbb1..906a283 100644 --- a/templates/oauth2/verify.twig +++ b/templates/oauth2/verify.twig @@ -1,6 +1,6 @@ {% extends 'oauth2/master.twig' %} -{% set body_header_class = 'devicehead' %} +{% set body_header_icon = 'code' %} {% set body_header_text = 'Code authorisation' %} {% set body_title = 'Authorisation Request' %}