├── .gitignore ├── composer.json ├── src ├── OAuthFactory.php ├── UserServiceInterface.php └── OAuthMiddleware.php ├── composer.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slimphp-api/slim-oauth", 3 | "type": "library", 4 | "description": "Slim Framework OAuth Middleware", 5 | "keywords": ["slim","framework","middleware","oauth"], 6 | "homepage": "http://slimframework.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Gabriel Baker", 11 | "email": "me@g403.co", 12 | "homepage": "http://g403.co" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.4.0", 17 | "psr/http-message": "1.*", 18 | "lusitanian/oauth": "~0.3" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "SlimApi\\OAuth\\": "src" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OAuthFactory.php: -------------------------------------------------------------------------------- 1 | serviceFactory = new ServiceFactory; 26 | $this->storage = new $this->storageClass(); 27 | $this->oAuthCredentials = $oAuthCredentials; 28 | } 29 | 30 | /** 31 | * Create an oauth service based on type 32 | * 33 | * @param string $type the type of oauth services to create 34 | */ 35 | public function createService($type, $scopes = ['user']) 36 | { 37 | $typeLower = strtolower($type); 38 | 39 | if (!array_key_exists($typeLower, $this->oAuthCredentials)) { 40 | return false; 41 | } 42 | 43 | // Create a new instance of the URI class with the current URI, stripping the query string 44 | $uriFactory = new UriFactory(); 45 | $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); 46 | $currentUri->setQuery(''); 47 | 48 | // Setup the credentials for the requests 49 | $credentials = new Credentials( 50 | $this->oAuthCredentials[$typeLower]['key'], 51 | $this->oAuthCredentials[$typeLower]['secret'], 52 | $currentUri->getAbsoluteUri() . '/callback' 53 | ); 54 | 55 | // Instantiate the OAuth service using the credentials, http client and storage mechanism for the token 56 | $this->registeredService = $this->serviceFactory->createService($type, $credentials, $this->storage, $scopes); 57 | } 58 | 59 | /** 60 | * if we don't have a registered service we attempt to make one 61 | * 62 | * @param string $type the oauth provider type 63 | * 64 | * @return OAuthService the created service 65 | */ 66 | public function getOrCreateByType($type) 67 | { 68 | if (! $this->registeredService) { 69 | $this->createService($type); 70 | } 71 | 72 | return $this->registeredService; 73 | } 74 | 75 | /** 76 | * retrieve the registered service 77 | * 78 | * @return OAuthService the registered oauth service 79 | */ 80 | public function getService() 81 | { 82 | return $this->registeredService; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/UserServiceInterface.php: -------------------------------------------------------------------------------- 1 | request('user') will result in thhe following 12 | * what is done with it and how it is stored is down to implementation 13 | * 14 | * This is also the place you should be checking if this user is actually allowed to be created, 15 | * checking if the user belongs to the right org or team. Doesn't matter on an open site, 16 | * but would matter for a org specific api 17 | * 18 | * { 19 | * "login": "octocat", 20 | * "id": 1, 21 | * "avatar_url": "https://github.com/images/error/octocat_happy.gif", 22 | * "gravatar_id": "", 23 | * "url": "https://api.github.com/users/octocat", 24 | * "html_url": "https://github.com/octocat", 25 | * "followers_url": "https://api.github.com/users/octocat/followers", 26 | * "following_url": "https://api.github.com/users/octocat/following{/other_user}", 27 | * "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 28 | * "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 29 | * "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 30 | * "organizations_url": "https://api.github.com/users/octocat/orgs", 31 | * "repos_url": "https://api.github.com/users/octocat/repos", 32 | * "events_url": "https://api.github.com/users/octocat/events{/privacy}", 33 | * "received_events_url": "https://api.github.com/users/octocat/received_events", 34 | * "type": "User", 35 | * "site_admin": false, 36 | * "name": "monalisa octocat", 37 | * "company": "GitHub", 38 | * "blog": "https://github.com/blog", 39 | * "location": "San Francisco", 40 | * "email": "octocat@github.com", 41 | * "hireable": false, 42 | * "bio": "There once was...", 43 | * "public_repos": 2, 44 | * "public_gists": 1, 45 | * "followers": 20, 46 | * "following": 0, 47 | * "created_at": "2008-01-14T04:33:35Z", 48 | * "updated_at": "2008-01-14T04:33:35Z", 49 | * "total_private_repos": 100, 50 | * "owned_private_repos": 100, 51 | * "private_gists": 81, 52 | * "disk_usage": 10000, 53 | * "collaborators": 8, 54 | * "plan": { 55 | * "name": "Medium", 56 | * "space": 400, 57 | * "private_repos": 20, 58 | * "collaborators": 0 59 | * } 60 | * } 61 | * 62 | * @param ServiceInterface $service oauth service 63 | */ 64 | public function createUser(ServiceInterface $service); 65 | 66 | /** 67 | * Create a user object, whether it's blank or filled, 68 | * $authToken is header from Authorization so you can retrieve from db 69 | * or some kind of in-memory storage redis etc 70 | * 71 | * @param string|false $authToken The auth token from the Authorization header 72 | */ 73 | public function findOrNew($authToken); 74 | } 75 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "385808926be74341a3bcb5c4e80b92dd", 8 | "packages": [ 9 | { 10 | "name": "lusitanian/oauth", 11 | "version": "v0.3.5", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Lusitanian/PHPoAuthLib.git", 15 | "reference": "ac5a1cd5a4519143728dce2213936eea302edf8a" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Lusitanian/PHPoAuthLib/zipball/ac5a1cd5a4519143728dce2213936eea302edf8a", 20 | "reference": "ac5a1cd5a4519143728dce2213936eea302edf8a", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "3.7.*", 28 | "predis/predis": "0.8.*@dev", 29 | "symfony/http-foundation": "~2.1" 30 | }, 31 | "suggest": { 32 | "ext-openssl": "Allows for usage of secure connections with the stream-based HTTP client.", 33 | "predis/predis": "Allows using the Redis storage backend.", 34 | "symfony/http-foundation": "Allows using the Symfony Session storage backend." 35 | }, 36 | "type": "library", 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "0.1-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-0": { 44 | "OAuth": "src", 45 | "OAuth\\Unit": "tests" 46 | } 47 | }, 48 | "notification-url": "https://packagist.org/downloads/", 49 | "license": [ 50 | "MIT" 51 | ], 52 | "authors": [ 53 | { 54 | "name": "David Desberg", 55 | "email": "david@daviddesberg.com" 56 | }, 57 | { 58 | "name": "Pieter Hordijk", 59 | "email": "info@pieterhordijk.com" 60 | } 61 | ], 62 | "description": "PHP 5.3+ oAuth 1/2 Library", 63 | "keywords": [ 64 | "Authentication", 65 | "authorization", 66 | "oauth", 67 | "security" 68 | ], 69 | "time": "2014-09-05 15:19:58" 70 | }, 71 | { 72 | "name": "psr/http-message", 73 | "version": "1.0", 74 | "source": { 75 | "type": "git", 76 | "url": "https://github.com/php-fig/http-message.git", 77 | "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" 78 | }, 79 | "dist": { 80 | "type": "zip", 81 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 82 | "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 83 | "shasum": "" 84 | }, 85 | "require": { 86 | "php": ">=5.3.0" 87 | }, 88 | "type": "library", 89 | "extra": { 90 | "branch-alias": { 91 | "dev-master": "1.0.x-dev" 92 | } 93 | }, 94 | "autoload": { 95 | "psr-4": { 96 | "Psr\\Http\\Message\\": "src/" 97 | } 98 | }, 99 | "notification-url": "https://packagist.org/downloads/", 100 | "license": [ 101 | "MIT" 102 | ], 103 | "authors": [ 104 | { 105 | "name": "PHP-FIG", 106 | "homepage": "http://www.php-fig.org/" 107 | } 108 | ], 109 | "description": "Common interface for HTTP messages", 110 | "keywords": [ 111 | "http", 112 | "http-message", 113 | "psr", 114 | "psr-7", 115 | "request", 116 | "response" 117 | ], 118 | "time": "2015-05-04 20:22:00" 119 | } 120 | ], 121 | "packages-dev": [], 122 | "aliases": [], 123 | "minimum-stability": "stable", 124 | "stability-flags": [], 125 | "prefer-stable": false, 126 | "prefer-lowest": false, 127 | "platform": { 128 | "php": ">=5.4.0" 129 | }, 130 | "platform-dev": [] 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slim Framework OAuth Middleware 2 | 3 | [![Code Climate](https://codeclimate.com/github/slimphp-api/slim-oauth/badges/gpa.svg)](https://codeclimate.com/github/slimphp-api/slim-oauth) 4 | 5 | 6 | This repository contains a Slim Framework OAuth middleware. 7 | 8 | Enables you to authenticate using various OAuth providers. 9 | 10 | The middleware allows registration with various oauth services and uses a user service to register/retrieve the user details. 11 | After registration/authentication it responds with a Authorization header which it expects to be returned as is to authorise further requests. 12 | It's up to the supplied user service how this is accomplished. 13 | 14 | ## Installation 15 | 16 | Via Composer 17 | 18 | ```bash 19 | $ composer require slimphp-api/slim-oauth 20 | ``` 21 | 22 | Requires Slim 3.0.0 or newer. 23 | 24 | ## Usage 25 | 26 | ```php 27 | getContainer(); 35 | 36 | // these should all probably be in some configuration class 37 | $container['oAuthCreds'] = [ 38 | 'github' => [ 39 | 'key' => 'abc', 40 | 'secret' => '123', 41 | ] 42 | ]; 43 | 44 | $container[SlimApi\OAuth\OAuthFactory::class] = function($container) 45 | { 46 | return new OAuthFactory($container->get('oAuthCreds')); 47 | }; 48 | 49 | $container[SlimApi\OAuth\UserServiceInterface::class] = function($container) 50 | { 51 | //user service should implement SlimApi\OAuth\UserServiceInterface 52 | //user model should have a token variable to hold the random token sent to the client 53 | return new Foo\Service\UserService($container->get('Foo\Model\User')); 54 | }; 55 | 56 | $container[SlimApi\OAuth\OAuthMiddleware::class] = function($container) 57 | { 58 | return new OAuthMiddleware($container->get('SlimApi\OAuth\OAuthFactory'), $container->get('SlimApi\OAuth\UserServiceInterface')); 59 | }; 60 | 61 | $app->add($container->get('SlimApi\OAuth\OAuthMiddleware')); 62 | 63 | $app->run(); 64 | ``` 65 | 66 | Example user service 67 | 68 | ```php 69 | userModel = $userModel; 80 | } 81 | 82 | public function createUser(ServiceInterface $service) 83 | { 84 | // request the user information from github 85 | // could go further with this and check org/team membership 86 | $user = json_decode($service->request('user'), true); 87 | 88 | // try to find user by the oauth server's user id, 89 | // best way since oauth token might have been invalidated 90 | $models = $this->userModel->byRemoteId($user['id'])->get(); 91 | $model = $models->first(); 92 | 93 | if (!$model) { 94 | // create and save a new user 95 | $model = new $this->userModel([ 96 | 'remote_id' => $user['id'] 97 | ]); 98 | } 99 | $model->oauth_token = $service->getStorage()->retrieveAccessToken('GitHub')->getAccessToken(); 100 | $model->token = 'randomstringj0'; // this isn't really random, but it should be! 101 | $model->save(); 102 | return $model; 103 | } 104 | 105 | public function findOrNew($authToken) 106 | { 107 | // retrieve the user by the authToken provided 108 | // this could also be from some fast access redis db 109 | $users = $this->userModel->byToken($authToken)->get(); 110 | $user = $users->first(); 111 | // or return a blank entry if it doesn't exist 112 | return ($user ?: new $this->userModel); 113 | } 114 | } 115 | ``` 116 | 117 | Once it's all configured redirecting the user to `https://domain/auth/?return=` 118 | where oauthtype is the service to authentication ie github and the return url parameter is where you want the user redirected to AFTER authentication. 119 | 120 | ## Process cycle 121 | 122 | ``` 123 | Client (https://www.example.com) requires the user to register/authenticate 124 | -> redirects to https://api.example.com/auth/github?return=https://www.example.com/authenticated 125 | -> api redirects to GitHub to authenticate 126 | -> GitHub asks user to verify 127 | -> GitHub redirects back to https://api.example.com/auth/github/callback with a temp code in the url 128 | -> api exchanges temp code for permanent token 129 | -> api asks user service to verify/store user and details and return user object (must have token param) 130 | -> api redirects back to client https://www.example.com/authenticated with an Authorization header `'token '.$user->token` 131 | -> client adds Authorization header to all subsequent requests 132 | -> api retrieves user object by Authorization token to check existence 133 | ``` 134 | 135 | ## Credits 136 | 137 | - [Gabriel Baker](https://github.com/gabriel403) 138 | 139 | ## License 140 | 141 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 142 | -------------------------------------------------------------------------------- /src/OAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | \w+)'; 18 | private static $callbackRoute = '/auth/(?\w+)/callback'; 19 | 20 | /** 21 | * @param OAuthFactory $oAuthFactory The OAuthFacotry instance to use 22 | * @param UserServiceInterface $userService 23 | * @param array $oAuthProviders An array of valid oauth providers 24 | */ 25 | public function __construct(OAuthFactory $oAuthFactory, UserServiceInterface $userService, $oAuthProviders = ['github']) 26 | { 27 | $this->oAuthFactory = $oAuthFactory; 28 | $this->userService = $userService; 29 | $this->oAuthProviders = $oAuthProviders; 30 | } 31 | 32 | /** 33 | * Invoke middleware 34 | * 35 | * @param RequestInterface $request PSR7 request object 36 | * @param ResponseInterface $response PSR7 response object 37 | * @param callable $next Next middleware callable 38 | * 39 | * @return ResponseInterface PSR7 response object 40 | */ 41 | public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next = null) 42 | { 43 | $returnValue = $this->checkForOAuthPaths($request, $response); 44 | 45 | // if not false, means we've got some redirecting to do 46 | if (false !== $returnValue) { 47 | return $returnValue; 48 | } 49 | 50 | // Fetches the current user or returns a default 51 | $authHeaders = $request->getHeader('Authorization'); 52 | $authValue = $this->parseForAuthentication($authHeaders); 53 | 54 | $user = $this->userService->findOrNew($authValue); 55 | $request = $request->withAttribute('user', $user); 56 | if ($user->token) { 57 | $response = $response->withHeader('Authorization', 'token '.$user->token); 58 | } 59 | 60 | if ($next) { 61 | $response = $next($request, $response); 62 | } 63 | 64 | return $response; 65 | } 66 | 67 | /** 68 | * Check the current url for oauth paths 69 | * 70 | * @param RequestInterface $request PSR7 request object 71 | * @param ResponseInterface $response PSR7 response object 72 | * 73 | * @return ResponseInterface|false PSR7 response object 74 | */ 75 | private function checkForOAuthPaths(RequestInterface $request, ResponseInterface $response) 76 | { 77 | $path = $request->getUri()->getPath(); 78 | 79 | if (!is_string($path)) { 80 | return false; 81 | } 82 | 83 | // this matches the request to authenticate for an oauth provider 84 | if (1 === preg_match($this->getAuthRouteRegex(), $path, $matches)) { 85 | // validate we have an allowed oAuthServiceType 86 | if (!in_array($matches['oAuthServiceType'], $this->oAuthProviders)) { 87 | throw new Exception("Unknown oAuthServiceType"); 88 | } 89 | 90 | // validate the return url 91 | parse_str($_SERVER['QUERY_STRING'], $query); 92 | if (!array_key_exists('return', $query) || filter_var($query['return'], FILTER_VALIDATE_URL) === false) { 93 | throw new Exception("Invalid return url"); 94 | } 95 | 96 | $_SESSION['oauth_return_url'] = $query['return']; 97 | 98 | $url = $this->oAuthFactory->getOrCreateByType($matches['oAuthServiceType'])->getAuthorizationUri(); 99 | 100 | return $response->withStatus(302)->withHeader('Location', $url); 101 | } elseif (1 === preg_match($this->getCallbackRouteRegex(), $path, $matches)) { // this matches the request to post-authentication for an oauth provider 102 | if (!in_array($matches['oAuthServiceType'], $this->oAuthProviders)) { 103 | throw new Exception("Unknown oAuthServiceType"); 104 | } 105 | 106 | $service = $this->oAuthFactory->getOrCreateByType($matches['oAuthServiceType']); 107 | // turn our code into a token that's stored internally 108 | $service->requestAccessToken($request->getParam('code')); 109 | // validates and creates the user entry in the db if not already exists 110 | $user = $this->userService->createUser($service); 111 | // set our token in the header and then redirect to the client's chosen url 112 | return $response->withStatus(200)->withHeader('Authorization', 'token '.$user->token)->withHeader('Location', $_SESSION['oauth_return_url']); 113 | } 114 | 115 | return false; 116 | } 117 | 118 | /** 119 | * Parse the Authorization header for auth tokens 120 | * 121 | * @param array $authHeaders Array of PSR7 headers specific to authorization 122 | * 123 | * @return string|false Return either the auth token of false if none found 124 | * 125 | */ 126 | private function parseForAuthentication(array $authHeaders) 127 | { 128 | $authValue = false; 129 | if (count($authHeaders) > 0) { 130 | foreach ($authHeaders as $authHeader) { 131 | $authValues = explode(' ', $authHeader); 132 | if (2 === count($authValues) && array_search(strtolower($authValues[0]), ['bearer', 'token'])) { 133 | $authValue = $authValues[1]; 134 | break; 135 | } 136 | } 137 | } 138 | return $authValue; 139 | } 140 | 141 | /** 142 | * convert the route to a regex 143 | * 144 | * @param string $route the route to convert 145 | * 146 | * @return string a regex of the route 147 | */ 148 | private function regexRoute($route) 149 | { 150 | return '@^' . $route . '$@'; 151 | } 152 | 153 | /** 154 | * get the regex for the route used to authenticate 155 | * 156 | * @return string the auth route regex 157 | */ 158 | private function getAuthRouteRegex() 159 | { 160 | return $this->regexRoute(static::$authRoute); 161 | } 162 | 163 | /** 164 | * get the regex for the callback route for authentication 165 | * 166 | * @return string regex route 167 | */ 168 | private function getCallbackRouteRegex() 169 | { 170 | return $this->regexRoute(static::$callbackRoute); 171 | } 172 | } 173 | --------------------------------------------------------------------------------