<?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"); } } }