Authentication
If your application has users, you maybe need to provide them a way to authenticate their Rest API requests, and also their websocket connections.
As Sandstone uses Symfony, you can securize RestAPI requests as described in the documentation. See Silex security.
For an authenticated websocket connection, Sandstone provide an OAuth2 provider based on the library thephpleague/oauth2-server.
Sandstone authentication
The Sandstone OAuth2 provider allows to use same authentication tokens for a Rest API request and a websocket connection.
The idea is that you first get an OAuth access token from the Rest API using your username and password. You then receive an access token you can use to:
- authenticate your Rest API requests by adding it in headers:
GET /api/hello
Authorization: Bearer L83pR5amKt
- authenticate your websocket connection at the opening:
websocket.connect('ws://localhost:8482?access_token=L83pR5amKt')
Implementation
Symfony authentication
If you already configured your Symfony security firewall, you can skip this step and use your own configuration.
Otherwise, let’s implement a basic authentication system. Here using Symfony’s
InMemoryUserProvider
, I’ll create a hardcoded user in configuration.
Up to you to implement your own Symfony UserProvider
, see
How to Create a custom User Provider.
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Silex\Provider\SecurityServiceProvider;
use Eole\Sandstone\Application as SandstoneApplication;
$app = new SandstoneApplication([
'project.root' => dirname(__DIR__),
'env' => 'dev',
'debug' => true,
]);
$app['app.user_provider'] = function () {
return new InMemoryUserProvider([
// username: admin / password: foo
'admin' => [
'roles' => ['ROLE_ADMIN'],
'password' => '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a',
],
]);
};
$app->register(new SecurityServiceProvider(), [
'security.firewalls' => [
'api' => [
'pattern' => '^/api',
'anonymous' => true,
'http' => true,
'users' => $app['app.user_provider'],
],
],
]);
$app->run();
I configured the api
firewall which securize all routes starting with /api
.
In my Sandstone application, it represents the whole Rest API.
This firewall allows anonymous requests ('anonymous' => true
),
so authentication is optional unless I add a condition in my controllers.
I also added
'http' => true
to allows basic authentication for the following example.
Note: This example is based on Silex security documentation. See Silex security.
Now I can already authenticate my Rest API requests. Let’s test it by
adding the GET /api/hello
route:
use Symfony\Component\HttpFoundation\JsonResponse;
$app->get('api/hello', function () use ($app) {
$result = [
'authenticated' => $app['user'] ? $app['user']->getUsername() : '*nope*',
];
return new JsonResponse($result);
});
If I requests it without authentication, I get:
# Request
GET /api/hello
# Response
{
"authenticated": "*nope*"
}
By authenticating admin:foo
with basic auth, I get:
# Request
GET /api/hello
Authorization: Basic YWRtaW46Zm9v
# Response
{
"authenticated": "admin"
}
The string
YWRtaW46Zm9v
is the base64 encoded ofadmin:foo
. See Basic access authentication.
Register Sandstone OAuth2
In order to authenticate websocket connections, you have to register Sandstone’s OAuth2 provider with some configuration:
use Eole\Sandstone\OAuth2\Silex\OAuth2ServiceProvider;
$app->register(new OAuth2ServiceProvider(), [
'oauth.firewall_name' => 'api',
'oauth.security.user_provider' => 'app.user_provider',
'oauth.tokens_dir' => $app['project.root'].'/var/oauth-tokens',
'oauth.scope' => [
'id' => 'sandstone-scope',
'description' => 'Sandstone scope.',
],
'oauth.clients' => [
'my-web-application' => [
'name' => 'my-app-name',
'id' => 'my-app',
'secret' => 'my-app-secret',
],
],
]);
Configuration details:
oauth.firewall_name
is the firewall to use to securize websocket connectionsoauth.security.user_provider
set your user provider service nameoauth.tokens_dir
folder to store oauth tokensoauth.scope
the scope of your oauth tokenoauth.clients
your api keys for clients who use oauth
You also need to update your firewall configuration to make authentication stateless and use oauth:
'security.firewalls' => [
'api' => [
// ...
'oauth' => true,
'stateless' => true,
],
],
Note: See Stateless Authentication on Silex documentation.
You can now test your OAuth, but you first need a way to issue an access token from the Rest API.
Create a controller to get an access token
Sandstone OAuth component provides a service to issue a token. You have to call
$app['sandstone.oauth.controller']->postAccessToken($request)
from a controller,
and return the token to the client.
Here is an example of a controller:
use League\OAuth2\Server\Exception\OAuthException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
$app->post('/oauth/access-token', function (Request $request) use ($app) {
try {
$token = $app['sandstone.oauth.controller']->postAccessToken($request);
return new JsonResponse($token);
} catch (OAuthException $e) {
return new JsonResponse([
'oauth_error_type' => $e->errorType,
'message' => $e->getMessage(),
'parameter' => $e->getParameter(),
'should_redirect' => $e->shouldRedirect(),
'redirect' => $e->redirectUri,
], $e->httpStatusCode);
}
});
Note: Make the access token route outside the
api
firewall so that getting an access token is not behind OAuth security. In this example, the access token route prefix is/oauth/
whereas the api routes prefixes are/api/
.
Get an OAuth access token
An access token is a string like VBLQa98foR2dOSJpb9bugh00e1r7H74manwEhmbj
.
It’s generated from the server, and associated to an user.
It has an expiration time, so you have to get another one before it expires.
Sandstone also uses refresh tokens. You then get also a refresh token you may store when you get an access token.
See Which OAuth 2.0 grant should I implement? from thephpleague oauth2 server documentation.
You can get an access token from your Rest API by using a grant type.
Password grant type
Get an access token with your username and password. It’s usefull for the first connection.
POST /oauth/access-token
Content-Type: application/x-www-form-urlencoded
grant_type=password&client_id=my-app&client_secret=my-app-secret&username=admin&password=foo
Response example:
{
"access_token": "VBLQa98foR2dOSJpb9bugh00e1r7H74manwEhmbj",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "JyrPAZZU17EDYIFSxnKmsX3HzwxGSW1fijEu5rsL"
}
Refresh Token grant type
Get an access token from your last refresh token. It’s usefull to not resend your secret credentials. Also you won’t need to store user password in your client application, but only the refresh token.
POST /oauth/access-token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&client_id=my-app&client_secret=my-app-secret&refresh_token=JyrPAZZU17EDYIFSxnKmsX3HzwxGSW1fijEu5rsL
You’ll get a similar response as the example above, with a new access token and refresh token.
Use the access token
In Rest API requests
GET /api/hello
Authorization: Bearer VBLQa98foR2dOSJpb9bugh00e1r7H74manwEhmbj
In websocket connection
websocket.connect('ws://localhost:8482?access_token=VBLQa98foR2dOSJpb9bugh00e1r7H74manwEhmbj')
You’ll then see in your websocket server logs:
[info] Authentication... []
[info] User logged. {"username":"alcalyn"}
Enable the security panel in the profiler
You can enable the Security panel in your Symfony profiler
by adding symfony/security-bundle
in your dependencies.
Just do:
composer require symfony/security-bundle
Make an Api call, then you should see the new panel by profiling the new request.
See on Github silexphp/Silex-WebProfiler
Full authenticated application example
Here is the full working example with an OAuth authentication.
require_once '../vendor/autoload.php';
use League\OAuth2\Server\Exception\OAuthException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Silex\Provider\SecurityServiceProvider;
use Eole\Sandstone\OAuth2\Silex\OAuth2ServiceProvider;
use Eole\Sandstone\Application as SandstoneApplication;
$app = new SandstoneApplication([
'project.root' => dirname(__DIR__),
'env' => 'dev',
'debug' => true,
]);
$app['app.user_provider'] = function () {
return new InMemoryUserProvider([
// username: admin / password: foo
'admin' => [
'roles' => ['ROLE_ADMIN'],
'password' => '$2y$10$3i9/lVd8UOFIJ6PAMFt8gu3/r5g0qeCJvoSlLCsvMTythye19F77a',
],
]);
};
$app->register(new SecurityServiceProvider(), [
'security.firewalls' => [
'api' => [
'pattern' => '^/api',
'oauth' => true,
'stateless' => true,
'anonymous' => true,
'users' => $app['app.user_provider'],
],
],
]);
$app->register(new OAuth2ServiceProvider(), [
'oauth.firewall_name' => 'api',
'oauth.security.user_provider' => 'app.user_provider',
'oauth.tokens_dir' => $app['project.root'].'/var/oauth-tokens',
'oauth.scope' => [
'id' => 'sandstone-scope',
'description' => 'Sandstone scope.',
],
'oauth.clients' => [
'my-web-application' => [
'name' => 'my-app-name',
'id' => 'my-app',
'secret' => 'my-app-secret',
],
],
]);
$app->post('/oauth/access-token', function (Request $request) use ($app) {
try {
$token = $app['sandstone.oauth.controller']->postAccessToken($request);
return new JsonResponse($token);
} catch (OAuthException $e) {
return new JsonResponse([
'oauth_error_type' => $e->errorType,
'message' => $e->getMessage(),
'parameter' => $e->getParameter(),
'should_redirect' => $e->shouldRedirect(),
'redirect' => $e->redirectUri,
], $e->httpStatusCode);
}
});
$app->get('api/hello', function () use ($app) {
$result = [
'authenticated' => $app['user'] ? $app['user']->getUsername() : '*nope*',
];
return new JsonResponse($result);
});
$app->run();
Here is the Postman collection:
sandstone_oauth.postman_collection.json
Postman allows to run presaved requests to an API. Import this file and you’ll get requests examples to your Sandstone application with OAuth.
Here is an example of websocket connection with an access token using Javascript:
ab.connect('ws://0.0.0.0:8482?access_token=L83pR5amKtRdaTo3hTBsaD7tp8tcWWCCsKgGMH9M')
I use here AutobahnJS 0.8