├── .gitignore ├── src ├── Exception │ ├── TokenRequestException.php │ ├── TokenStorageException.php │ └── AuthenticationException.php ├── Contract │ ├── Authenticator.php │ ├── JSONGetter.php │ └── JSONPoster.php ├── Http │ ├── routes.php │ └── Controllers │ │ └── AuthController.php ├── Adapter │ ├── NullAuthenticatorAdapter.php │ └── JSONFetcherAdapter.php ├── RequestTokenParser.php ├── TokenStorage.php ├── TokenRefresher.php ├── TokenMiddleware.php ├── ServiceProvider.php ├── KeysFetcher.php └── OIDConnectSocialiteProvider.php ├── config └── opidconnect.php ├── composer.json ├── migrations └── 2017_08_02_143801_create_tokens_table.php ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | Thumbs.db -------------------------------------------------------------------------------- /src/Exception/TokenRequestException.php: -------------------------------------------------------------------------------- 1 | '', 5 | 'client_secret' => '', 6 | 'redirect' => 'https://mysite.com/auth/callback', 7 | 'auth' => 'https://opidc.provider/auth', 8 | 'token' => 'https://opidc.provider/token', 9 | 'keys' => 'https://opidc.provider/keys', 10 | 'guzzle' => [], 11 | ]; 12 | -------------------------------------------------------------------------------- /src/Contract/JSONGetter.php: -------------------------------------------------------------------------------- 1 | string('sub'); 18 | $table->string('iss'); 19 | $table->string('refresh_token'); 20 | $table->index(['sub', 'iss']); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('tokens'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Artemiy Ryabinkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/RequestTokenParser.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 28 | } 29 | 30 | /** 31 | * @param Request $request 32 | * 33 | * @return Token 34 | */ 35 | public function parse(Request $request): Token 36 | { 37 | $bearer = $request->headers->get(static::AUTH_HEADER); 38 | 39 | if (empty($bearer)) { 40 | throw new AuthenticationException("Request doesn't contain auth token"); 41 | } 42 | 43 | $parts = explode(" ", $bearer); 44 | 45 | if (count($parts) < 2) { 46 | throw new AuthenticationException("Invalid format of auth header"); 47 | } 48 | 49 | $jwt = $parts[1]; 50 | 51 | return $this->parser->parse($jwt); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/TokenStorage.php: -------------------------------------------------------------------------------- 1 | query = $query; 21 | } 22 | 23 | /** 24 | * Save refresh token in DB 25 | * 26 | * @param string $sub 27 | * @param string $iss 28 | * @param string $refreshToken 29 | * @return bool 30 | */ 31 | public function saveRefresh(string $sub, string $iss, string $refreshToken): bool 32 | { 33 | return $this->query->getConnection() 34 | ->table("tokens") 35 | ->updateOrInsert([ 36 | 'sub' => $sub, 37 | 'iss' => $iss, 38 | ], [ 39 | 'refresh_token' => $refreshToken, 40 | ]); 41 | } 42 | 43 | 44 | /** 45 | * Fetch and return Refresh token by iss and sub params 46 | * 47 | * @param string $sub 48 | * @param string $iss 49 | * @return null|string 50 | */ 51 | public function fetchRefresh(string $sub, string $iss): ?string 52 | { 53 | /* @var \Illuminate\Support\Collection $list */ 54 | $list = $this->query->getConnection() 55 | ->table("tokens") 56 | ->select(['refresh_token']) 57 | ->where('sub', $sub) 58 | ->where('iss', $iss) 59 | ->limit(1) 60 | ->get(); 61 | 62 | if ($list->isEmpty()) { 63 | return null; 64 | } 65 | 66 | return $list->first()->refresh_token; 67 | } 68 | } -------------------------------------------------------------------------------- /src/Adapter/JSONFetcherAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function get(string $url, array $params = [], array $options = []): array 30 | { 31 | $reqOpts = array_merge([ 32 | 'query' => $params, 33 | 'headers' => [ 34 | 'Accept' => 'application/json', 35 | ], 36 | ], $options); 37 | 38 | return $this->request("GET", $url, $reqOpts); 39 | } 40 | 41 | /** 42 | * @param string $method 43 | * @param string $url 44 | * @param array $options 45 | * 46 | * @return array 47 | */ 48 | protected function request(string $method, string $url, array $options): array 49 | { 50 | $response = $this->client->request($method, $url, $options); 51 | 52 | // TODO: Handle request errors (ex.: authorization error with 403 status code) 53 | 54 | $data = $response->getBody()->getContents(); 55 | 56 | return json_decode($data, true); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function post(string $url, array $params = [], $body = null, array $options = []): array 63 | { 64 | $reqOpts = array_merge([ 65 | 'query' => $params, 66 | 'headers' => [ 67 | 'Accept' => 'application/json', 68 | 'Content-Type' => 'application/x-www-form-urlencoded', 69 | ], 70 | 'body' => $body, 71 | ], $options); 72 | 73 | return $this->request("POST", $url, $reqOpts); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/TokenRefresher.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 53 | $this->clientId = $clientId; 54 | $this->clientSecret = $clientSecret; 55 | $this->redirectUrl = $redirectUrl; 56 | $this->poster = $poster; 57 | $this->tokenUrl = $tokenUrl; 58 | } 59 | 60 | /** 61 | * @param string $sub 62 | * @param string $iss 63 | * 64 | * @return string 65 | */ 66 | public function refreshIDToken(string $sub, string $iss): string 67 | { 68 | $refreshToken = $this->storage->fetchRefresh($sub, $iss); 69 | 70 | if (!$refreshToken) { 71 | throw new TokenStorageException("Failed to fetch refresh token"); 72 | } 73 | 74 | $data = $this->poster->post($this->tokenUrl, [], http_build_query([ 75 | 'client_id' => $this->clientId, 76 | 'client_secret' => $this->clientSecret, 77 | 'grant_type' => 'refresh_token', 78 | 'refresh_token' => $refreshToken, 79 | 'redirect_uri' => $this->redirectUrl, 80 | 'scope' => implode(' ', $this->scopes), 81 | ])); 82 | 83 | if (!$this->storage->saveRefresh($sub, $iss, $data['refresh_token'])) { 84 | throw new TokenStorageException("Failed to store refresh token"); 85 | } 86 | 87 | return $data['id_token']; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TokenMiddleware.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 66 | $this->validator = $validator; 67 | $this->clock = $clock; 68 | $this->signer = $signer; 69 | $this->keysFetcher = $keysFetcher; 70 | $this->tokenRefresher = $tokenRefresher; 71 | $this->authenticator = $authenticator; 72 | } 73 | 74 | /** 75 | * Validate an ID Token of incoming request. 76 | * 77 | * @param \Illuminate\Http\Request $request 78 | * @param \Closure $next 79 | * 80 | * @return mixed 81 | * @throws \Furdarius\OIDConnect\Exception\AuthenticationException 82 | */ 83 | public function handle($request, Closure $next) 84 | { 85 | /** 86 | * We cant get claims from Token interface, so call claims method implicitly 87 | * link: https://github.com/lcobucci/jwt/pull/186 88 | * 89 | * @var $token \Lcobucci\JWT\Token\Plain 90 | */ 91 | $token = $this->parser->parse($request); 92 | 93 | $kid = $token->headers()->get('kid'); 94 | $key = $this->keysFetcher->getByKID($kid); 95 | 96 | if (!$key) { 97 | throw new AuthenticationException("Public Key doesn't exist"); 98 | } 99 | 100 | if (!$this->validator->validate($token, new SignedWith($this->signer, $key))) { 101 | throw new AuthenticationException("Token sign is invalid"); 102 | } 103 | 104 | if (!$this->validator->validate($token, new ValidAt($this->clock))) { 105 | throw new AuthenticationException("Token is expired"); 106 | } 107 | 108 | $this->authenticator->authUser($token->claims()); 109 | 110 | return $next($request); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | stateless()->redirect(); 25 | 26 | return $redirectResponse; 27 | } 28 | 29 | /** 30 | * @param Request $request 31 | * @param \Furdarius\OIDConnect\TokenStorage $storage 32 | * 33 | * @return \Illuminate\Http\JsonResponse 34 | */ 35 | public function callback(Request $request, TokenStorage $storage) 36 | { 37 | // TODO: handle CORS more elegant way 38 | if ($request->getMethod() === 'OPTIONS') { 39 | return $this->responseJson([]) 40 | ->header('Access-Control-Allow-Origin', '*') 41 | ->header('Access-Control-Allow-Methods', strtoupper($request->headers->get('Access-Control-Request-Method'))) 42 | ->header('Access-Control-Allow-Headers', $request->headers->get('Access-Control-Request-Headers')); 43 | } 44 | 45 | /** @var \Laravel\Socialite\Two\User $user */ 46 | $user = \Socialite::with('myoidc')->stateless()->user(); 47 | 48 | if (!$storage->saveRefresh($user['sub'], $user['iss'], $user->refreshToken)) { 49 | throw new TokenStorageException("Failed to save refresh token"); 50 | } 51 | 52 | return $this->responseJson([ 53 | 'name' => $user->getName(), 54 | 'email' => $user->getEmail(), 55 | 'token' => $user->token, 56 | ]); 57 | } 58 | 59 | /** 60 | * @param array|\JsonSerializable $data 61 | * @param int $status 62 | * @param array $headers 63 | * 64 | * @return JsonResponse 65 | */ 66 | protected function responseJson($data, int $status = 200, array $headers = []): JsonResponse 67 | { 68 | return response()->json($data, $status, $headers) 69 | ->setEncodingOptions(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) 70 | ->header('Access-Control-Allow-Origin', '*'); 71 | } 72 | 73 | /** 74 | * @param Request $request 75 | * @param TokenRefresher $refresher 76 | * @param Parser $parser 77 | * 78 | * @return AuthenticationException|JsonResponse 79 | */ 80 | public function refresh(Request $request, TokenRefresher $refresher, Parser $parser) 81 | { 82 | $data = $request->json()->all(); 83 | 84 | if (!isset($data['token'])) { 85 | return new AuthenticationException("Failed to get JWT token from input"); 86 | } 87 | 88 | $jwt = $data['token']; 89 | /** 90 | * We cant get claims from Token interface, so call claims method implicitly 91 | * link: https://github.com/lcobucci/jwt/pull/186 92 | * 93 | * @var $token \Lcobucci\JWT\Token\Plain 94 | */ 95 | $token = $parser->parse($jwt); 96 | 97 | $claims = $token->claims(); 98 | 99 | $sub = $claims->get('sub'); 100 | $iss = $claims->get('iss'); 101 | 102 | $refreshedIDToken = $refresher->refreshIDToken($sub, $iss); 103 | 104 | return $this->responseJson([ 105 | 'token' => $refreshedIDToken, 106 | ]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([realpath(__DIR__ . '/../config/opidconnect.php') => config_path('opidconnect.php')]); 35 | $this->loadMigrationsFrom(realpath(__DIR__ . '/../migrations')); 36 | $this->loadRoutesFrom(realpath(__DIR__ . '/Http/routes.php')); 37 | 38 | $socialite = $this->app->make(SocialiteFactory::class); 39 | 40 | $socialite->extend( 41 | 'myoidc', 42 | function ($app) use ($socialite) { 43 | $config = $app['config']['opidconnect']; 44 | 45 | return new OIDConnectSocialiteProvider( 46 | $app[Request::class], 47 | $app[Parser::class], 48 | $config['client_id'], 49 | $config['client_secret'], 50 | $config['redirect'], 51 | $config['auth'], 52 | $config['token'] 53 | ); 54 | } 55 | ); 56 | } 57 | 58 | /** 59 | * Register any application services. 60 | * 61 | * @return void 62 | */ 63 | public function register() 64 | { 65 | $this->app->singleton(JSONFetcherAdapter::class, function ($app) { 66 | $config = $app['config']['opidconnect']['guzzle']; 67 | 68 | $cl = new Client($config); 69 | 70 | return new JSONFetcherAdapter($cl); 71 | }); 72 | 73 | $this->app->singleton(JSONGetter::class, function ($app) { 74 | return $app[JSONFetcherAdapter::class]; 75 | }); 76 | 77 | $this->app->singleton(JSONPoster::class, function ($app) { 78 | return $app[JSONFetcherAdapter::class]; 79 | }); 80 | 81 | $this->app->singleton(Decoder::class, function ($app) { 82 | return new JsonDecoder(); 83 | }); 84 | 85 | $this->app->singleton(Parser::class, function ($app) { 86 | return new Token\Parser($app[Decoder::class]); 87 | }); 88 | 89 | $this->app->singleton(Validator::class, function ($app) { 90 | return new JWTValidator(); 91 | }); 92 | 93 | $this->app->singleton(Clock::class, function ($app) { 94 | return new SystemClock(); 95 | }); 96 | 97 | $this->app->singleton(Signer::class, function ($app) { 98 | return new Signer\Rsa\Sha256(); 99 | }); 100 | 101 | $this->app->bind(KeysFetcher::class, function ($app) { 102 | $config = $app['config']['opidconnect']; 103 | 104 | return new KeysFetcher( 105 | $app[JSONGetter::class], 106 | $app['cache.store'], 107 | $app[Decoder::class], 108 | $config['keys'] 109 | ); 110 | }); 111 | 112 | $this->app->bind(TokenRefresher::class, function ($app) { 113 | $config = $app['config']['opidconnect']; 114 | 115 | return new TokenRefresher( 116 | $app[JSONPoster::class], 117 | $app[TokenStorage::class], 118 | $config['client_id'], 119 | $config['client_secret'], 120 | $config['redirect'], 121 | $config['token'] 122 | ); 123 | }); 124 | 125 | $this->app->singleton(Authenticator::class, function ($app) { 126 | return new NullAuthenticatorAdapter(); 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/KeysFetcher.php: -------------------------------------------------------------------------------- 1 | fetcher = $fetcher; 41 | $this->cache = $cache; 42 | $this->jwksURI = $jwksURI; 43 | $this->decoder = $decoder; 44 | } 45 | 46 | /** 47 | * Fetch JWK key from JWKs URI with defined kid 48 | * 49 | * @param string $kid 50 | * 51 | * @return Key|null 52 | */ 53 | public function getByKID(string $kid): ?Key 54 | { 55 | $cacheKey = 'keys.' . $kid; 56 | 57 | if ($this->cache->has($cacheKey)) { 58 | return $this->cache->get($cacheKey); 59 | } 60 | 61 | /** @var Key[] $keys */ 62 | $keys = $this->fetch(); 63 | 64 | if (!isset($keys[$kid])) { 65 | return null; 66 | } 67 | 68 | $this->cache->put($cacheKey, $keys[$kid], Carbon::now()->addHours(6)); 69 | 70 | return $keys[$kid]; 71 | } 72 | 73 | /** 74 | * Fetch list of JWKs from JWKs URI 75 | * 76 | * @return array 77 | */ 78 | public function fetch(): array 79 | { 80 | $result = []; 81 | 82 | $data = $this->fetcher->get($this->jwksURI); 83 | foreach ($data['keys'] as $key) { 84 | $result[$key['kid']] = new Key($this->createPemFromModulusAndExponent($key['n'], $key['e'])); 85 | } 86 | 87 | return $result; 88 | } 89 | 90 | 91 | /** 92 | * 93 | * Create a public key represented in PEM format from RSA modulus and exponent information 94 | * 95 | * @param string $n the RSA modulus encoded in URL Safe Base64 96 | * @param string $e the RSA exponent encoded in URL Safe Base64 97 | * 98 | * @return string the RSA public key represented in PEM format 99 | */ 100 | protected function createPemFromModulusAndExponent(string $n, string $e): string 101 | { 102 | $modulus = $this->decoder->base64UrlDecode($n); 103 | $publicExponent = $this->decoder->base64UrlDecode($e); 104 | 105 | $components = [ 106 | 'modulus' => pack('Ca*a*', 2, $this->encodeLength(strlen($modulus)), $modulus), 107 | 'publicExponent' => pack('Ca*a*', 2, $this->encodeLength(strlen($publicExponent)), $publicExponent), 108 | ]; 109 | 110 | $RSAPublicKey = pack( 111 | 'Ca*a*a*', 112 | 48, 113 | $this->encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), 114 | $components['modulus'], 115 | $components['publicExponent'] 116 | ); 117 | 118 | $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); 119 | $RSAPublicKey = chr(0) . $RSAPublicKey; 120 | $RSAPublicKey = chr(3) . $this->encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; 121 | $RSAPublicKey = pack( 122 | 'Ca*a*', 123 | 48, 124 | $this->encodeLength(strlen($rsaOID . $RSAPublicKey)), 125 | $rsaOID . $RSAPublicKey 126 | ); 127 | 128 | $RSAPublicKey = "-----BEGIN PUBLIC KEY-----" . PHP_EOL 129 | . chunk_split(base64_encode($RSAPublicKey), 64, PHP_EOL) 130 | . '-----END PUBLIC KEY-----'; 131 | 132 | return $RSAPublicKey; 133 | } 134 | 135 | /** 136 | * DER-encode the length 137 | * 138 | * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See 139 | * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} 140 | * for more information. 141 | * 142 | * @param int $length 143 | * 144 | * @return string 145 | */ 146 | protected function encodeLength(int $length): string 147 | { 148 | if ($length <= 0x7F) { 149 | return chr($length); 150 | } 151 | 152 | $temp = ltrim(pack('N', $length), chr(0)); 153 | return pack('Ca*', 0x80 | strlen($temp), $temp); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/OIDConnectSocialiteProvider.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 73 | $this->authUrl = $authUrl; 74 | $this->tokenUrl = $tokenUrl; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function user() 81 | { 82 | if ($this->hasInvalidState()) { 83 | throw new InvalidStateException; 84 | } 85 | 86 | $response = $this->getAccessTokenResponse($this->getCode()); 87 | 88 | if (!empty($response['error'])) { 89 | throw new TokenRequestException($response['error']); 90 | } 91 | 92 | $token = $response['id_token']; 93 | 94 | $user = $this->mapUserToObject($this->getUserByToken($token)); 95 | 96 | return $user->setToken($token) 97 | ->setRefreshToken(Arr::get($response, 'refresh_token')) 98 | ->setExpiresIn(Arr::get($response, 'expires_in')); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | protected function mapUserToObject(array $user) 105 | { 106 | return (new User)->setRaw($user)->map([ 107 | 'id' => $user['sub'], 108 | 'sub' => $user['sub'], 109 | 'iss' => $user['iss'], 110 | 'nickname' => $user['name'], 111 | 'name' => $user['name'], 112 | 'email' => $user['email'], 113 | ]); 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | protected function getUserByToken($token) 120 | { 121 | /** 122 | * We cant get claims from Token interface, so call claims method implicitly 123 | * link: https://github.com/lcobucci/jwt/pull/186 124 | * 125 | * @var $plainToken \Lcobucci\JWT\Token\Plain 126 | */ 127 | $plainToken = $this->parser->parse($token); 128 | 129 | $claims = $plainToken->claims(); 130 | 131 | return [ 132 | 'sub' => $claims->get('sub'), 133 | 'iss' => $claims->get('iss'), 134 | 'name' => $claims->get('name'), 135 | 'email' => $claims->get('email'), 136 | ]; 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | protected function getTokenFields($code) 143 | { 144 | return [ 145 | 'client_id' => $this->clientId, 146 | 'client_secret' => $this->clientSecret, 147 | 'code' => $code, 148 | 'redirect_uri' => $this->redirectUrl, 149 | 'grant_type' => 'authorization_code', 150 | ]; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | protected function getAuthUrl($state) 157 | { 158 | return $this->buildAuthUrlFromBase($this->authUrl, $state); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | protected function getTokenUrl() 165 | { 166 | return $this->tokenUrl; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://habrastorage.org/web/7c1/a19/e76/7c1a19e76cf54cb1adf2217a156b7310.png) 2 | 3 | The OpenIDConnect Laravel package is meant to provide you an opportunity to easily authenticate users using OpenID Connect protocol. 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/furdarius/oidconnect-laravel/v/stable)](https://packagist.org/packages/furdarius/oidconnect-laravel) 6 | [![Latest Unstable Version](https://poser.pugx.org/furdarius/oidconnect-laravel/v/unstable)](https://packagist.org/packages/furdarius/oidconnect-laravel) 7 | [![Total Downloads](https://poser.pugx.org/furdarius/oidconnect-laravel/downloads)](https://packagist.org/packages/furdarius/oidconnect-laravel) 8 | [![License](https://poser.pugx.org/furdarius/oidconnect-laravel/license)](https://packagist.org/packages/furdarius/oidconnect-laravel) 9 | 10 | ## Installation 11 | 12 | To install this package you will need: 13 | * Laravel 5.4+ 14 | * PHP 7.1+ 15 | 16 | Use composer to install 17 | ```bash 18 | composer require furdarius/oidconnect-laravel:dev-master 19 | ``` 20 | 21 | Open `config/app.php` and register the required service providers above your application providers. 22 | ```php 23 | 'providers' => [ 24 | ... 25 | Laravel\Socialite\SocialiteServiceProvider::class, 26 | Furdarius\OIDConnect\ServiceProvider::class 27 | ... 28 | ] 29 | ``` 30 | 31 | If you'd like to make configuration changes in the configuration file you can pubish it with the following Aritsan command: 32 | ```bash 33 | php artisan vendor:publish --provider="Furdarius\OIDConnect\ServiceProvider" 34 | ``` 35 | 36 | After that, roll up migrations: 37 | ```bash 38 | php artisan migrate 39 | ``` 40 | 41 | ## Usage 42 | 43 | 44 | #### Configuration 45 | At first you will need to add credentials for the OpenID Connect service your application utilizes. 46 | These credentials should be placed in your `config/opidconnect.php` configuration file. 47 | 48 | ```php 49 | 'CLIENT_ID_HERE', 53 | 'client_secret' => 'CLIENT_SECRET_HERE', 54 | 'redirect' => env('APP_URL') . '/auth/callback', 55 | 'auth' => 'https://oidc.service.com/auth', 56 | 'token' => 'https://oidc.service.com/token', 57 | 'keys' => 'https://oidc.service.com/keys', 58 | ]; 59 | ``` 60 | 61 | #### Endpoints 62 | Now, your app has auth endpoints: 63 | * `GET /auth/redirect` - Used to redirect client to Auth Service login page. 64 | * `GET /auth/callback` - Used when Auth Service redirect client to callback url with code. 65 | * `POST /auth/refresh` - Used by client for ID Token refreshing. 66 | 67 | #### Middleware 68 | You need to use Auth Middleware on protected routes. 69 | Open `App\Http\Kernel` and register middleware in `$routeMiddleware`: 70 | ```php 71 | protected $routeMiddleware = [ 72 | 'token' => \Furdarius\OIDConnect\TokenMiddleware::class 73 | ]; 74 | ``` 75 | 76 | And then use it as usual: 77 | ```php 78 | Route::middleware('token')->get('/protected', function (Illuminate\Http\Request $request) { 79 | return "You are on protected zone"; 80 | }); 81 | ``` 82 | 83 | #### User Auth 84 | 85 | Create your own `StatelessGuard` and setup it in `config/auth.php`. Example: 86 | 87 | Guard: 88 | ```php 89 | user) { 110 | throw new AuthenticationException('Unauthenticated user'); 111 | } 112 | 113 | return $this->user; 114 | } 115 | 116 | /** 117 | * @param array $credentials 118 | * @return bool 119 | */ 120 | public function validate(array $credentials = []) 121 | { 122 | return $this->user instanceof Authenticatable; 123 | } 124 | } 125 | ``` 126 | 127 | Config (`config/auth.php`): 128 | 129 | ```php 130 | 'defaults' => [ 131 | 'guard' => 'stateless', 132 | 'passwords' => 'users', 133 | ], 134 | 135 | ... 136 | 137 | 'guards' => [ 138 | 'stateless' => [ 139 | 'driver' => 'stateless' 140 | ] 141 | ], 142 | ``` 143 | 144 | 145 | Then implement own `Authenticator`. Example: 146 | 147 | ```php 148 | get('email'); 167 | if (!$email) { 168 | throw new AuthenticationException('User\'s email not present in token'); 169 | } 170 | 171 | $model = new User(['email' => $email]); 172 | 173 | \Auth::setUser($model); 174 | } 175 | } 176 | ``` 177 | 178 | And implement auth guard service provider. Example: 179 | 180 | ```php 181 | app->singleton(Authenticator::class, function ($app) { 210 | return new PersonAuthenticatorAdapter(); 211 | }); 212 | } 213 | } 214 | ``` 215 | 216 | Then register it in `config/app.php`: 217 | 218 | ``` 219 | 'providers' => [ 220 | ... 221 | App\Auth\AuthenticatorServiceProvider::class, 222 | ... 223 | ] 224 | ``` 225 | 226 | Now you can use `\Auth::user();` for getting current user information. --------------------------------------------------------------------------------