HTTP Skeleton for OAuth2 implementation.
This commit is contained in:
parent
dc363f1854
commit
865c48d26d
2 changed files with 241 additions and 0 deletions
|
@ -71,6 +71,8 @@ class HanyuuContext {
|
|||
return 503;
|
||||
});
|
||||
|
||||
$routingCtx->register(new OAuth2\OAuth2Routes);
|
||||
|
||||
return $routingCtx;
|
||||
}
|
||||
}
|
||||
|
|
239
src/OAuth2/OAuth2Routes.php
Normal file
239
src/OAuth2/OAuth2Routes.php
Normal file
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
namespace Hanyuu\OAuth2;
|
||||
|
||||
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler};
|
||||
|
||||
final class OAuth2Routes extends RouteHandler {
|
||||
#[HttpGet('/oauth2/authorise')]
|
||||
public function getAuthorise($response, $request) {
|
||||
$type = (string)$request->getParam('response_type');
|
||||
if($type !== 'code')
|
||||
return 400; // TODO: descriptive/better looking error notice
|
||||
|
||||
$state = (string)$request->getParam('state');
|
||||
if($state === '') // technically state is not required by the spec but its probably best to enforce it
|
||||
return 400; // TODO: ^
|
||||
|
||||
$clientId = (string)$request->getParam('client_id');
|
||||
if($clientId === 'wrong')
|
||||
return 404; // TODO: is this the correct error code for this situation?
|
||||
|
||||
$scope = explode(' ', (string)$request->getParam('scope'));
|
||||
if(in_array('invalid', $scope))
|
||||
return 403; // TODO: ensure the set of scopes we have is valid for this application
|
||||
|
||||
$codeChallengeMethod = (string)$request->getParam('code_challenge_method');
|
||||
if($codeChallengeMethod === '')
|
||||
$codeChallengeMethod = 'plain';
|
||||
elseif(!in_array($codeChallengeMethod, ['plain', 'S256']))
|
||||
return 400; // TODO: see $type check
|
||||
|
||||
$codeChallenge = (string)$request->getParam('code_challenge');
|
||||
if($codeChallengeMethod !== 'plain' && $codeChallenge === '')
|
||||
return 400; // TODO: see $type check, not technically part of the spec but if you're bothering to specify a method then like ???
|
||||
elseif($codeChallengeMethod === 'S256' && strlen($codeChallenge) !== 32)
|
||||
return 400; // TODO: see $type check, code_challenge should always be 32 chars long if S256
|
||||
|
||||
// optional, yoink default from database
|
||||
$redirectUri = (string)$request->getParam('redirect_uri');
|
||||
if($redirectUri !== '') {
|
||||
if($redirectUri === 'bad')
|
||||
return 403; // TODO: if specified, ensure the redirect_uri is allowed, make sure to do this again in the token request
|
||||
}
|
||||
|
||||
// display confirm diag
|
||||
// upon yes, redir to whatever with the code and the state
|
||||
}
|
||||
|
||||
private static function error(string $code, string $message = '', string $url = ''): array {
|
||||
$info = ['error' => $code];
|
||||
if($message !== '')
|
||||
$info['error_description'] = $message;
|
||||
if($url !== '')
|
||||
$info['error_uri'] = $url;
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
#[HttpPost('/oauth2/authorise-device')]
|
||||
public function postAuthoriseDevice($response, $request) {
|
||||
if(!$request->isFormContent()) {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
|
||||
}
|
||||
|
||||
$content = $request->getContent();
|
||||
|
||||
$clientId = (string)$content->getParam('client_id');
|
||||
if($clientId === 'wrong') { // verify and look up
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_client', 'No application has been registered with this client id.');
|
||||
}
|
||||
|
||||
$scope = explode(' ', (string)$content->getParam('scope'));
|
||||
if(in_array('invalid', $scope)) {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_scope', 'An invalid scope was requested.');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'device_code' => 'soapsoapsoapsoapsoapsoapsoapsoap',
|
||||
'user_code' => 'SOAP-504P',
|
||||
'verification_uri' => 'https://fii.moe/auth',
|
||||
'verification_uri_complete' => 'https://fii.moe/auth?code=SOAP-504P',
|
||||
'expires_in' => 1800,
|
||||
];
|
||||
|
||||
// in case interval isn't 5
|
||||
$result['interval'] = 5;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[HttpOptions('/oauth2/token')]
|
||||
#[HttpPost('/oauth2/token')]
|
||||
public function postToken($response, $request) {
|
||||
$originHeaders = ['Origin', 'X-Origin', 'Referer'];
|
||||
$origins = [];
|
||||
foreach($originHeaders as $originHeader) {
|
||||
$originHeader = $request->getHeaderFirstLine($originHeader);
|
||||
if($originHeader !== '' && !in_array($originHeader, $origins))
|
||||
$origins[] = $originHeader;
|
||||
}
|
||||
|
||||
if(!empty($origins)) {
|
||||
// TODO: check if none of the provided origins is on a blocklist or something
|
||||
// different origins being specified for each header should probably also be considered suspect...
|
||||
|
||||
$response->setHeader('Access-Control-Allow-Origin', $origins[0]);
|
||||
$response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST');
|
||||
$response->setHeader('Access-Control-Allow-Headers', 'Authorization');
|
||||
$response->setHeader('Access-Control-Expose-Headers', 'Vary');
|
||||
foreach($originHeaders as $originHeader)
|
||||
$response->setHeader('Vary', $originHeader);
|
||||
}
|
||||
|
||||
if($request->getMethod() === 'OPTIONS')
|
||||
return 204;
|
||||
|
||||
if(!$request->isFormContent()) {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_request', 'Your request must use content type application/x-www-form-urlencoded.');
|
||||
}
|
||||
|
||||
$content = $request->getContent();
|
||||
|
||||
$authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization'));
|
||||
if($authzHeader[0] === 'Basic') {
|
||||
$authzHeader = explode(':', base64_decode($authzHeader[1] ?? ''));
|
||||
$clientId = $authzHeader[0];
|
||||
$clientSecret = $authzHeader[1] ?? '';
|
||||
} elseif($authzHeader[0] !== '') {
|
||||
$response->setStatusCode(401);
|
||||
$message = 'You must either use the Basic method for Authorization or use the client_id and client_secret parameters.';
|
||||
$response->setHeader('WWW-Authenticate', "Basic realm=\"{$message}\"");
|
||||
return self::error('invalid_client', $message);
|
||||
} else {
|
||||
$clientId = (string)$content->getParam('client_id');
|
||||
$clientSecret = (string)$content->getParam('client_secret');
|
||||
}
|
||||
|
||||
if($clientId === 'wrong') { // verify and look up
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_client', 'No application has been registered with this client id.');
|
||||
}
|
||||
|
||||
$type = (string)$content->getParam('grant_type');
|
||||
if($type === 'authorization_code') {
|
||||
$code = (string)$content->getParam('code');
|
||||
|
||||
// require a code verifier be used if client_secret is not supplied or if the field for it is populated
|
||||
// error code for this is: invalid_request
|
||||
$codeVerifier = (string)$content->getParam('code_verifier');
|
||||
|
||||
// required if it was specified in the authorise req, check!
|
||||
// error code for this is also likely: invalid_request
|
||||
$redirectUri = (string)$content->getParam('redirect_uri');
|
||||
} elseif($type === 'refresh_token') {
|
||||
$refreshToken = (string)$content->getParam('refresh_token');
|
||||
|
||||
// should not contain more than the original access_token, prolly just omit
|
||||
$scope = explode(' ', (string)$content->getParam('scope'));
|
||||
|
||||
// i'm honestly not sure if this is the proper place for it but
|
||||
if(in_array('invalid', $scope)) {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_scope', 'An invalid scope was requested.');
|
||||
}
|
||||
} elseif($type === 'client_credentials') {
|
||||
// uses client_secret
|
||||
} elseif($type === 'password') {
|
||||
if($clientId !== 'flashii') {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('unauthorized_client', 'This application is not allowed t ouse this grant type.');
|
||||
}
|
||||
|
||||
// this should only be allowed for certain applications
|
||||
$userName = (string)$content->getParam('username');
|
||||
$password = (string)$content->getParam('password');
|
||||
} elseif($type === 'device_code' || $type === 'urn:ietf:params:oauth:grant-type:device_code') {
|
||||
$deviceCode = (string)$content->getParam('device_code');
|
||||
|
||||
if($deviceCode === 'expired') {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('expired_token', 'This device code has expired.');
|
||||
}
|
||||
|
||||
if($deviceCode === 'speedy') {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('slow_down', 'You are polling too fast, please increase your interval by 5 seconds.');
|
||||
}
|
||||
|
||||
if($deviceCode === 'denied') {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('access_denied', 'User has rejected authorisation attempt.');
|
||||
}
|
||||
|
||||
if($deviceCode === 'pending') {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('authorization_pending', 'User has not yet completed authorisation, check again in a bit.');
|
||||
}
|
||||
} else {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('unsupported_grant_type', 'Requested grant type is not supported by this server.');
|
||||
}
|
||||
|
||||
if($clientId === 'invalid_grant') {
|
||||
$response->setStatusCode(400);
|
||||
|
||||
if($type === 'refresh_token')
|
||||
$message = 'Failed to request new access token. Provided refresh token has likely expired or is invalid.';
|
||||
elseif($type === 'password')
|
||||
$message = 'Failed to request access token, user name or password was incorrect.';
|
||||
elseif($type === 'client_credentials')
|
||||
$message = 'Failed to request application access token.';
|
||||
else
|
||||
$message = 'Failed to request access token.';
|
||||
|
||||
return self::error('invalid_grant', $message);
|
||||
}
|
||||
|
||||
// TODO: ensure the set of requested scopes is still valid for this application
|
||||
$scope ??= [];
|
||||
if(in_array('invalid', $scope)) {
|
||||
$response->setStatusCode(400);
|
||||
return self::error('invalid_scope', 'One or more requested scopes are no longer valid for this application, please restart authorisation.');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'access_token' => 'abcdef',
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => 3600, // should be bigger on the server side to offer leeway
|
||||
];
|
||||
|
||||
// only if refreshable
|
||||
$result['refresh_token'] = 'ghijkl';
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue