Select Git revision
-
Adam Paquette authoredAdam Paquette authored
OAuth2RequestHandler.php 6.70 KiB
<?php
namespace RAP;
use \Firebase\JWT\JWT;
class OAuth2RequestHandler {
private $locator;
public function __construct(\RAP\Locator $locator) {
$this->locator = $locator;
}
public function handleAuthorizeRequest($params) {
if ($params['client_id'] === null) {
throw new BadRequestException("Client id is required");
}
if ($params['redirect_uri'] === null) {
throw new BadRequestException("Redirect URI is required");
}
$client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($params['client_id']);
if ($client === null) {
throw new BadRequestException("Invalid client id: " . $params['client_id']);
}
if ($client->redirectUrl !== $params['redirect_uri']) {
throw new BadRequestException("Invalid client redirect URI: " . $params['redirect_uri']);
}
$alg = $params['alg'];
if ($alg === null) {
$alg = "RS256";
}
$state = $params['state'];
if ($state === null) {
throw new BadRequestException("State is required");
}
// Storing OAuth2 data in session
$oauth2Data = new \RAP\OAuth2Data();
$oauth2Data->clientId = $client->client;
$oauth2Data->redirectUrl = $client->redirectUrl;
$oauth2Data->state = $state;
$scope = $params['scope'];
if ($scope !== null) {
$oauth2Data->scope = explode(' ', $scope);
}
$session = $this->locator->getSession();
$session->setOAuth2Data($oauth2Data);
}
public function getCodeResponseUrl(): string {
$session = $this->locator->getSession();
$accessToken = new \RAP\AccessToken();
$accessToken->code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64)));
$accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128)));
$accessToken->userId = $session->getUser()->id;
$accessToken->clientId = $session->getOAuth2Data()->clientId;
$accessToken->redirectUri = $session->getOAuth2Data()->redirectUrl;
$accessToken->scope = $session->getOAuth2Data()->scope;
$this->locator->getAccessTokenDAO()->createAccessToken($accessToken);
$state = $session->getOAuth2Data()->state;
$redirectUrl = $session->getOAuth2Data()->redirectUrl
. '?code=' . $accessToken->code . '&scope=profile&state=' . $state;
return $redirectUrl;
}
public function handleAccessTokenRequest($params): array {
$this->validateAccessTokenRequest($params);
$accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($params['code']);
if ($accessToken === null) {
throw new BadRequestException("No token for given code");
}
if ($accessToken->redirectUri !== $params['redirect_uri']) {
throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']);
}
$result = [];
$result['access_token'] = $accessToken->token;
$result['token_type'] = 'Bearer';
$result['expires_in'] = $this->getExpiresIn($accessToken);
if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) {
$result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
}
return $result;
}
private function validateAccessTokenRequest($params) {
if ($params['grant_type'] === null) {
throw new BadRequestException("grant_type is required");
} else if ($params['grant_type'] !== 'authorization_code') {
throw new BadRequestException("grant_type must be authorization_code");
}
if ($params['code'] === null) {
throw new BadRequestException("code id is required");
}
if ($params['redirect_uri'] === null) {
throw new BadRequestException("Redirect URI is required");
}
// Note: theorically the standard wants also the client_id here,
// however some clients don't send it
}
public function handleCheckTokenRequest($token): array {
if (!isset($_POST['token'])) {
throw new BadRequestException("Access token id is required");
}
$accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token);
$user = $this->locator->getUserDAO()->findUserById($accessToken->userId);
$result = [];
$result['exp'] = $this->getExpiresIn($accessToken);
$result['user_name'] = $user->id;
$result['client_id'] = $accessToken->clientId;
if ($accessToken->scope !== null) {
$result['scope'] = $accessToken->scope;
if (in_array('openid', $accessToken->scope)) {
$result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
}
}
return $result;
}
private function getExpiresIn(AccessToken $accessToken) {
$expTime = strtotime($accessToken->expirationTime);
$now = time();
return $expTime - $now;
}
public function validateToken(): void {
$headers = apache_request_headers();
if (!isset($headers['Authorization'])) {
throw new BadRequestException("Missing Authorization header");
}
$authorizationHeader = explode(" ", $headers['Authorization']);
if ($authorizationHeader[0] === "Bearer") {
$bearer_token = $authorizationHeader[1];
} else {
throw new BadRequestException("Invalid token type");
}
$accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token);
if ($accessToken === null) {
$this->attemptJWTTokenValidation($bearer_token);
} else if ($accessToken->expired) {
throw new UnauthorizedException("Access token is expired");
}
}
private function attemptJWTTokenValidation($jwt): void {
$jwtParts = explode('.', $jwt);
if (count($jwtParts) === 0) {
throw new UnauthorizedException("Invalid token");
}
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0]));
if (!isset($header->kid)) {
throw new UnauthorizedException("Invalid token: missing kid in header");
}
$keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid);
if ($keyPair === null) {
throw new UnauthorizedException("Invalid kid: no key found");
}
try {
JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]);
} catch (\Firebase\JWT\ExpiredException $ex) {
throw new UnauthorizedException("Access token is expired");
}
}
}