├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── routes └── web.php └── src ├── Http └── Controllers │ └── IntrospectionController.php └── Providers ├── OAuthIntrospectionServiceProvider.php └── RouteProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | .idea 4 | composer.lock 5 | 6 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 8 | # composer.lock 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 ipunkt Business Solutions OHG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth 2.0 Token Introspection 2 | 3 | [![Total Downloads](https://poser.pugx.org/ipunkt/laravel-oauth-introspection/d/total.svg)](https://packagist.org/packages/ipunkt/laravel-oauth-introspection) 4 | [![Latest Stable Version](https://poser.pugx.org/ipunkt/laravel-oauth-introspection/v/stable.svg)](https://packagist.org/packages/ipunkt/laravel-oauth-introspection) 5 | [![Latest Unstable Version](https://poser.pugx.org/ipunkt/laravel-oauth-introspection/v/unstable.svg)](https://packagist.org/packages/ipunkt/laravel-oauth-introspection) 6 | [![License](https://poser.pugx.org/ipunkt/laravel-oauth-introspection/license.svg)](https://packagist.org/packages/ipunkt/laravel-oauth-introspection) 7 | 8 | ## Introduction 9 | 10 | OAuth 2.0 Introspection extends Laravel Passport to separate the authorization server and the resource server. 11 | 12 | To verify an access token at the resource server the client sends it as bearer token to the resource server and the resource server makes an introspection server-to-server call to verify data and signature of the given token. 13 | 14 | ## Installation 15 | 16 | Just install the package on your authorization server 17 | 18 | composer require ipunkt/laravel-oauth-introspection 19 | 20 | and add the Service Provider in your `config/app.php` 21 | 22 | \Ipunkt\Laravel\OAuthIntrospection\Providers\OAuthIntrospectionServiceProvider::class, 23 | 24 | ## Official Documentation 25 | 26 | Documentation for OAuth 2.0 Token Introspection can be found on the [RFC 7662](https://tools.ietf.org/html/rfc7662). 27 | 28 | ## License 29 | 30 | OAuth 2.0 Token Introspection is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipunkt/laravel-oauth-introspection", 3 | "description": "OAuth 2.0 Token Introspection implementation for extending Laravel Passport (RFC 7662)", 4 | "version": "2.0.1", 5 | "keywords": [ 6 | "laravel", 7 | "passport", 8 | "oauth", 9 | "introspection", 10 | "rfc-7662" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Robert Kummer", 16 | "email": "r.kummer@ipunkt.biz" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.0", 21 | "guzzlehttp/guzzle": ">=7.0", 22 | "ipunkt/laravel-package-manager": "^2.0", 23 | "laminas/laminas-diactoros": "^2.4", 24 | "laravel/passport": ">=8.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Ipunkt\\Laravel\\OAuthIntrospection\\": "src/" 29 | } 30 | }, 31 | "config": { 32 | "preferred-install": "dist", 33 | "sort-packages": true, 34 | "optimize-autoloader": true 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "Ipunkt\\Laravel\\OAuthIntrospection\\Providers\\OAuthIntrospectionServiceProvider" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | jwt = $jwt; 58 | $this->resourceServer = $resourceServer; 59 | $this->accessTokenRepository = $accessTokenRepository; 60 | $this->clientRepository = $clientRepository; 61 | } 62 | 63 | /** 64 | * Authorize a client to access the user's account. 65 | * 66 | * @param ServerRequestInterface $request 67 | * 68 | * @return JsonResponse|ResponseInterface 69 | */ 70 | public function introspectToken(ServerRequestInterface $request) 71 | { 72 | try { 73 | $this->resourceServer->validateAuthenticatedRequest($request); 74 | 75 | if (Arr::get($request->getParsedBody(), 'token_type_hint', 'access_token') !== 'access_token') { 76 | // unsupported introspection 77 | return $this->notActiveResponse(); 78 | } 79 | 80 | $accessToken = Arr::get($request->getParsedBody(), 'token'); 81 | if ($accessToken === null) { 82 | return $this->notActiveResponse(); 83 | } 84 | 85 | $token = $this->jwt->parse($accessToken); 86 | if (!$this->verifyToken($token)) { 87 | return $this->errorResponse([ 88 | 'error' => [ 89 | 'title' => 'Token invalid' 90 | ] 91 | ]); 92 | } 93 | 94 | /** @var string $userModel */ 95 | $userModel = config('auth.providers.users.model'); 96 | $user = (new $userModel)->find($token->getClaim('sub')); 97 | 98 | return $this->jsonResponse([ 99 | 'active' => true, 100 | 'scope' => trim(implode(' ', (array)$token->getClaim('scopes', []))), 101 | 'client_id' => $token->getClaim('aud'), 102 | 'username' => optional($user)->email, 103 | 'token_type' => 'access_token', 104 | 'exp' => intval($token->getClaim('exp')), 105 | 'iat' => intval($token->getClaim('iat')), 106 | 'nbf' => intval($token->getClaim('nbf')), 107 | 'sub' => $token->getClaim('sub'), 108 | 'aud' => $token->getClaim('aud'), 109 | 'jti' => $token->getClaim('jti'), 110 | ]); 111 | } catch (OAuthServerException $oAuthServerException) { 112 | return $oAuthServerException->generateHttpResponse(new Psr7Response); 113 | } catch (\Exception $exception) { 114 | return $this->exceptionResponse($exception); 115 | } 116 | } 117 | 118 | /** 119 | * returns inactive token message 120 | * 121 | * @return \Illuminate\Http\JsonResponse 122 | */ 123 | private function notActiveResponse() : JsonResponse 124 | { 125 | return $this->jsonResponse(['active' => false]); 126 | } 127 | 128 | /** 129 | * @param array|mixed $data 130 | * @param int $status 131 | * 132 | * @return \Illuminate\Http\JsonResponse 133 | */ 134 | private function jsonResponse($data, $status = 200) : JsonResponse 135 | { 136 | return new JsonResponse($data, $status); 137 | } 138 | 139 | private function verifyToken(Token $token) : bool 140 | { 141 | $signer = new \Lcobucci\JWT\Signer\Rsa\Sha256(); 142 | $publicKey = 'file://' . Passport::keyPath('oauth-public.key'); 143 | 144 | try { 145 | if (!$token->verify($signer, $publicKey)) { 146 | return false; 147 | } 148 | 149 | $data = new ValidationData(); 150 | $data->setCurrentTime(time()); 151 | 152 | if (!$token->validate($data)) { 153 | return false; 154 | } 155 | 156 | // is token revoked? 157 | if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) { 158 | return false; 159 | } 160 | 161 | if ($this->clientRepository->revoked($token->getClaim('aud'))) { 162 | return false; 163 | } 164 | 165 | return true; 166 | } catch (\Exception $exception) { 167 | } 168 | 169 | return false; 170 | } 171 | 172 | /** 173 | * @param array $data 174 | * @param int $status 175 | * 176 | * @return \Illuminate\Http\JsonResponse 177 | */ 178 | private function errorResponse($data, $status = 400) : JsonResponse 179 | { 180 | return $this->jsonResponse($data, $status); 181 | } 182 | 183 | /** 184 | * returns an error 185 | * 186 | * @param \Exception $exception 187 | * @param int $status 188 | * 189 | * @return \Illuminate\Http\JsonResponse 190 | */ 191 | private function exceptionResponse(\Exception $exception, $status = 500) : JsonResponse 192 | { 193 | return $this->errorResponse([ 194 | 'error' => [ 195 | 'id' => Str::slug(get_class($exception) . ' ' . $status), 196 | 'status' => $status, 197 | 'title' => $exception->getMessage(), 198 | 'detail' => $exception->getTraceAsString() 199 | ], 200 | ], $status); 201 | } 202 | } -------------------------------------------------------------------------------- /src/Providers/OAuthIntrospectionServiceProvider.php: -------------------------------------------------------------------------------- 1 |