usage of oauth-scope-approval
APPROVAL_URI and ERROR_URI constant are pointed to "oauth-scope-approval"
i would like to know the usage implementing this route.
please help
besides, i cannot find where the client_secret to set and get
please could you help?
Do you mean KEY_APPROVAL_URI_STRING? After a 3rd party server redirects a user to limoncello server it should show the user a list of scopes/rights/permissions requested by the 3rd party server. The user can a) accept all requested scopes b) edit and grant only a few scopes c) reject the request. The approval URI value is used in createAskResourceOwnerForApprovalResponse where it packs all the data in the URL and makes a redirect. It is used in Code Grant and Implicit Grant. The part you'are asking for is described in 4.1.1 of the RFC as
the authorization server authenticates the resource owner and obtains an authorization decision (by asking the resource owner or by establishing approval via other means).
The form should take the data (requested scope, if it midified from the default for this client, client state, etc) and present it to the user. Note you should check it was redirected from your server as rogue clients may bypass you and redirect clients directly to this form. If the resource owner grants access (see 4.1.2) then server should redirect the user to 3rd party server.
It's called clientCredentials. You can set the value on when create a client. By default it is verified with
PHP password_verify method in \Limoncello\Passport\Integration\BasePassportServerIntegration::verifyClientCredentials so it is expected to be password hash made by default PHP methods. Though you can replace verifyClientCredentials with custom implementation and store anything you want in credentials.
sorry. i found i was still confused about the setting in code grant basis on limoncello-app
to my understanding :
- i got 2 limoncello-app (A&B), A as a resource owner (http://localhost:8001), B as a authentication server (http://localhost)
- authorize via the http://localhost/authorize?client_id=default_client&redirect_url=http://localhost:8001
- get me to "APPROVAL_URI"
- any errors in between will get me to "ERROR_URI"
am i wrong?
on point 3, i am not sure if limoncello-app has given me the ability to approve. Is it built-in already or i have to build one for approving request?
i did some search on laravel about similar scenario, that is what i found:
Requesting Tokens
Redirecting For Authorization
Once a client has been created, developers may use their client ID and secret to request an authorization code and access token from your application. First, the consuming application should make a redirect request to your application's /oauth/authorize route like so:
Route::get('/redirect', function () {
$query = http_build_query([
'client_id' => 'client-id',
'redirect_uri' => 'http://example.com/callback',
'response_type' => 'code',
'scope' => '',
]);
return redirect('http://your-app.com/oauth/authorize?'.$query);
});
Remember, the /oauth/authorize route is already defined by the Passport::routes method. You do not need to manually define this route.
Approving The Request
When receiving authorization requests, Passport will automatically display a template to the user allowing them to approve or deny the authorization request. If they approve the request, they will be redirected back to the redirect_uri that was specified by the consuming application. The redirect_uri must match the redirect URL that was specified when the client was created.
If you would like to customize the authorization approval screen, you may publish Passport's views using the vendor:publish Artisan command. The published views will be placed in resources/views/vendor/passport:
php artisan vendor:publish --tag=passport-views
Converting Authorization Codes To Access Tokens
If the user approves the authorization request, they will be redirected back to the consuming application. The consumer should then issue a POST request to your application to request an access token. The request should include the authorization code that was issued by your application when the user approved the authorization request. In this example, we'll use the Guzzle HTTP library to make the POST request:
Route::get('/callback', function (Request $request) {
$http = new GuzzleHttp\Client;
$response = $http->post('http://your-app.com/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'redirect_uri' => 'http://example.com/callback',
'code' => $request->code,
],
]);
return json_decode((string) $response->getBody(), true);
});
This /oauth/token route will return a JSON response containing access_token, refresh_token, and expires_in attributes. The expires_in attribute contains the number of seconds until the access token expires.
Like the /oauth/authorize route, the /oauth/token route is defined for you by the Passport::routes method. There is no need to manually define this route.
i think there was no route and method being implemented for KEY_APPROVAL_URI_STRING which defaults to "oauth-scope-auth" for approving request, as laravel passport do.
Will you consider putting similar view/interface in limoncello-app?
i search through the limoncello app and framework, still cannot locate the method for Authorization Response. can you help?
I haven't tried to use limoncello in this scenario. Is it possible for you to publish a test project where I can test it and made necessary changes in the framework?
Another thing I observed is that. While I am doing code grant authorization. I can make authorize request at /authorize endpoint. But I have no where to generate a authorization code thru oauth-scope-approval. Is there some where I could do similar to authenticateByUserId. So then I could proceed to get the token from /token endpoint.
@dreamsbond I'm extremely busy this week and most of the next week. If you create a test project with 2 folders client and server each of them configured as an OAuth client and OAuth server and show me where you've got an issue I think it can be quickly fixed.
after a more deeper looked into the framework, i wrote a few lines to create authorization code as: https://github.com/dreamsbond/app/blob/dd3dbf0368069b91bea5ca2959546bc7ce49e434/server/app/Web/Controllers/AuthController.php#L205-L237


i can then reached the redirect_uri callback.
and the authorization code was successfully written:

so now, i have no idea about how could i make the authorization code expose as the query parameters into the redirect_url and also how to talk to the /token for authorization code <-> token exchange.
the flow for me to follow on Code Grant:

I'll help you. I'm creating a simple client and a server that work with OAuth. Though I cannot help as quickly as I usually do as I'm very busy until the end of the month, however, I hope I should get some time in the evenings.
I think I start managed to work with the client (webpack) and linkage between the view and twig.
But I still was not yet managed to work with managing scopes and make approval thru client like in laravel passport. It beyond my understanding to the code in current stage. I will try look more deeper between limoncello app and framwork. Hope to get more inspiration.
This branch was not field tested so don't worry at the moment that you don't understand it. It's likely not to be as it should be. I'm currently looking into this issue and making tweaks here and there to make code authorization sensible.
I have an initial version (very very beta really) of a server and a client.
If you run the server and login

and then open the client in a separate tab

when you click the login button it redirects you to the server where you're already logged in (sorry logging in after the redirect is not there for simplicity) you will see default scopes assigned to the client
(sorry for the fonts :smile: and absense of Deny button (again for simplicity) ) Also it shows 'sign-in' button though you're logged in and the server knows who you are.
When you click Confirm it generates an authorization code and redirects it back to client. The client handler currently fails but it's complete Part One. The part two should also be almost complete as exchange of the code has some tests.
I can publish it in its current state so you can play with it. Or you can wait until I polish it (though as I said I'm really busy for the next couple of weeks so it might take time even I think it's not much to do).

note the code is generated
thanks @neomerx. it is helpful to me to learn how to cope with the code and flow. i will go and checkout your update, hope to catch them up very soon.
oh sorry, i didn't read your message properly. i just thought your update was there. will be good if you don't mind sharing the codes in current stage so that i could try to catch and follow from scratch.
Hi, I've added some code to the client and it seems to work (still very very beta). After you click Confirm you will be redirected back to client and the client will exchange the auth code to tokens

the code is here https://github.com/neomerx/demo-oauth-server
The code has 2 commits
- Initial commit (2 default Limoncello apps).
- Server and client code added.
i am currently following your demo,
in the meantime, i looked into the method "setRedirectUriStrings()", setRedirectUriStrings was obsolete, redirect uri can only be effective thru $this->seedClient
for you information
September is over and I finally have a better schedule.
Back to setRedirectUriStrings this method is used to optimize reading clients. If you look at the database schema you will see that a client has relationships with other tables and data from those tables. Optimizations available for MySQL and PostgreSQL make it possible to read all data in one database query. Here you can see the usage of this method and here database migration that makes it possible.
besides the setRedirectUriStrings that i am look on.
I got problems redirecting to login page where user were not logged in.
here are the snippet:
public function createAskResourceOwnerForApprovalResponse(
string $type,
ClientInterface $client,
string $redirectUri = null,
bool $isScopeModified = false,
array $scopeList = null,
string $state = null,
array $extraParameters = []
): ResponseInterface
{
/** @var PassportAccountManagerInterface $manager */
$manager = $this->getContainer()->get(PassportAccountManagerInterface::class);
$passport = $manager->getPassport();
if ($passport === null) {
$signInUrl = static::createRouteUrl($this->getContainer(), AuthController::ROUTE_NAME_SIGN_IN);
return new RedirectResponse($signInUrl);
}
thru the methods in ControllerTrait;
I got an error there:
TypeError: Return value of Limoncello\Application\Http\RequestStorage::get() must implement interface Psr\Http\Message\ServerRequestInterface, null returned in D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\application\src\Http\RequestStorage.php:37
Stack trace:
#0 D:\github\dreamsbond\demo-oauth-server\server\server\app\Web\Controllers\ControllerTrait.php(418): Limoncello\Application\Http\RequestStorage->get()
#1 D:\github\dreamsbond\demo-oauth-server\server\server\app\Web\Controllers\ControllerTrait.php(400): class@anonymous::getHostUri(Object(Limoncello\Container\Container))
#2 D:\github\dreamsbond\demo-oauth-server\server\server\app\Container\OAuthConfigurator.php(57): class@anonymous::createRouteUrl(Object(Limoncello\Container\Container), 'auth_sign_in')
#3 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\passport\src\BasePassportServer.php(241): class@anonymous->createAskResourceOwnerForApprovalResponse('code', Object(Limoncello\Passport\Adaptors\Generic\Client), 'http://localhos...', true, Array, NULL, Array)
#4 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\oauth-server\src\GrantTraits\CodeGrantTrait.php(154): Limoncello\Passport\BasePassportServer->codeCreateAskResourceOwnerForApprovalResponse(Object(Limoncello\Passport\Adaptors\Generic\Client), 'http://localhos...', true, Array, NULL)
#5 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\passport\src\BasePassportServer.php(104): Limoncello\OAuthServer\BaseAuthorizationServer->codeAskResourceOwnerForApproval(Array, Object(Limoncello\Passport\Adaptors\Generic\Client), 'http://localhos...', NULL)
#6 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\oauth-server\src\BaseAuthorizationServer.php(77): Limoncello\Passport\BasePassportServer->createAuthorization(Array)
#7 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\passport\src\Package\PassportController.php(51): Limoncello\OAuthServer\BaseAuthorizationServer->getCreateAuthorization(Object(Zend\Diactoros\ServerRequest))
#8 [internal function]: Limoncello\Passport\Package\PassportController::authorize(Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#9 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(339): call_user_func(Array, Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#10 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(382): Limoncello\Core\Application\Application->callHandler(Array, Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#11 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\passport\src\Authentication\PassportMiddleware.php(81): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#12 [internal function]: Limoncello\Passport\Authentication\PassportMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#13 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(493): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#14 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\application\src\Packages\Session\SessionMiddleware.php(57): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#15 [internal function]: Limoncello\Application\Packages\Session\SessionMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#16 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(493): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#17 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\application\src\Packages\Cors\CorsMiddleware.php(57): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#18 [internal function]: Limoncello\Application\Packages\Cors\CorsMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#19 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(493): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#20 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\application\src\Packages\Cookies\CookieMiddleware.php(45): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#21 [internal function]: Limoncello\Application\Packages\Cookies\CookieMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#22 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(493): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#23 D:\github\dreamsbond\demo-oauth-server\server\server\app\Web\Middleware\CookieAuth.php(51): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#24 [internal function]: App\Web\Middleware\CookieAuth::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#25 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(493): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#26 [internal function]: Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#27 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(184): call_user_func(Object(Closure), Object(Zend\Diactoros\ServerRequest))
#28 D:\github\dreamsbond\demo-oauth-server\server\vendor\limoncello-php\core\src\Application\Application.php(141): Limoncello\Core\Application\Application->handleRequest(Object(Closure), Object(Zend\Diactoros\ServerRequest))
#29 D:\github\dreamsbond\demo-oauth-server\server\public\index.php(5): Limoncello\Core\Application\Application->run()
#30 {main}
I am going to make the redirect with a query parameter say "code_login=true", if the login checked code_login was present, it will pass to createAskResourceOwnerForApprovalResponse again for token exchange.
try out hardcoding uri
snippet:
public function createAskResourceOwnerForApprovalResponse(
string $type,
ClientInterface $client,
string $redirectUri = null,
bool $isScopeModified = false,
array $scopeList = null,
string $state = null,
array $extraParameters = []
): ResponseInterface
{
/** @var PassportAccountManagerInterface $manager */
$manager = $this->getContainer()->get(PassportAccountManagerInterface::class);
$passport = $manager->getPassport();
if ($passport === null) {
/** @var UriInterface $signInUri */
$signInUri = new Uri('http://localhost:8080/sign-in');
$signInWithQueryParamUri = $signInUri->withQuery(http_build_query([
'code_login' => true,
]));
return new RedirectResponse($signInWithQueryParamUri);
// return new TextResponse('Not yet implemented. Sorry you have to log-in into server first and then authenticate from client.', 400);
}
and
public static function authenticate(
/** @noinspection PhpUnusedParameterInspection */
array $routeParams,
ContainerInterface $container,
ServerRequestInterface $request
): ResponseInterface
{
$inputs = $request->getParsedBody();
$codeLogin = $request->getQueryParams()['code_login'] == 1;
if (is_array($inputs) === false) {
return new HtmlResponse(static::view($container, Views::SIGN_IN_PAGE, [
'error_message' => 'Invalid input data.',
'password_min_length' => User::MIN_PASSWORD_LENGTH,
]), 422);
}
// validate inputs
$formValidator = static::createFormValidator($container, SignIn::class);
if ($formValidator->validate($inputs) === false) {
/** @var Traversable $errorMessages */
$errorMessages = $formValidator->getMessages();
$errorMessages = iterator_to_array($errorMessages);
return new HtmlResponse(static::view($container, Views::SIGN_IN_PAGE, [
'errors' => $errorMessages,
'previous' => $inputs,
'password_min_length' => User::MIN_PASSWORD_LENGTH,
]), 422);
}
$captures = $formValidator->getCaptures();
list (self::FORM_EMAIL => $email, self::FORM_PASSWORD => $password) = $captures;
$isRemember = $captures[static::FORM_REMEMBER] ?? false;
assert(is_bool($isRemember));
// actual check for user email and password
/** @var PassportServerIntegrationInterface $passport */
$passport = $container->get(PassportServerIntegrationInterface::class);
$userId = $passport->validateUserId($email, $password);
if ($userId === null) {
return new HtmlResponse(static::view($container, Views::SIGN_IN_PAGE, [
'error_message' => 'Invalid email or password.',
'previous' => $inputs,
'password_min_length' => User::MIN_PASSWORD_LENGTH,
]), 401);
}
// if we are here name and password are valid.
// we have to create an auth token and return its value as a cookie.
return static::authenticateUserById(
$userId,
$isRemember,
$request->getQueryParams(),
static::getSettings($container, Authorization::class),
// static::createRouteUrl($container, HomeController::ROUTE_NAME_HOME),
$codeLogin === true ? 'http://localhost:8080/authorize?response_type=code&client_id=client1' : static::createRouteUrl($container, HomeController::ROUTE_NAME_HOME),
$passport,
$container->get(CookieJarInterface::class)
);
}
works as expected but ugly to get redirection...
another question in mind.
the authorization code granted will be permanent or temporary?
By default grant would be for 3600 seconds, then it should be prolonged with the refresh token.
The issue with making redirect URL is that you try to use ControllerTrait not from a controller but from middleware.
To remind you that a request goes through
app start -> container configurator 1 -> ... -> container configurator N -> middleware 1 -> ... -> middleware N -> Controller
then response goes back through all middlwares.
Method createRouteUrl depends on method getHostUri which in its turns depends on RememberRequestMiddleware which has to be very last middleware before controller. So when you use createRouteUrl from Passport middleware it fails because the current request is not remembered yet.
So what you can do?
- Get server base url (e.g. https://www.example.com) either from
$_SERVERor from settings like
/** @var CacheSettingsProviderInterface $provider */
$provider = $this->getContainer()->get(CacheSettingsProviderInterface::class);
$originUri = $provider->getApplicationConfiguration()[ApplicationConfigurationInterface::KEY_APP_ORIGIN_URI];
- Make an URL for login page
/** @var RouterInterface $router */
$router = $this->getContainer()->get(RouterInterface::class);
$signInUrl = $router->get($originUri, AuthController::ROUTE_NAME_SIGN_IN);
- Return redirect response.
Also you might want to make redirect back after authentication. If so you should add some parameter with return URL to sign-in redirect. Then update \App\Web\Controllers\AuthController::authenticate where you will read it and use as 5th parameter while calling authenticateUserById.
i have just updated my fork, followed your guideline and works.
i have another question. i am still curious about how limoncello(client) knows the user authenticated is eligible to access the protected resource? the fingerprint wasn't persist in limoncello(client). and roles or scopes of limoncello(client) may also varies to the oauth authentication server; limoncello (server)
The client knows the scopes allowed for that user. Having that knowledge client's developer can program the app accordingly.
The knowledge about the scope may come from places
- any client has a default scope set
- when a client gets token from the server it may have scope list (in case it was changed by the user and do not match the default set)
but on limoncello (client), i can see the validateScope is doing for scopes checking. but i checked that the oauth_token table behind do not have entry at all.
validateScope works with own scopes (client's which are not used) but you use auth/scopes of the server.
Since you use a server to auth, issue tokens, provide API and etc, the client only gets a token and use the server. The server itself check scopes, permissions and etc.
When a client requests a token it knows it asks for permissions to do actions A, B, C and etc. The developer of the client knows which API methods will be allowed if A granted, same for B and C. Depending on actual granted scopes (think of them as allowed actions which may map to just 1 method (e.g. CREATE_POST) or a group of methods (e.g. MANAGE_CONTENT for CRUD for posts and comments)).
I see. So the code grant approval achieved the delegation to access the protected resources in limoncello (server).
How would this associate with the limoncello (client) to access its local protected resource?
You can use the same approach we used in google auth. You can get from the server 'verified' email of the current user and then use the email to sign-in the user locally on the client so all security features of the client will be available for you.