app icon indicating copy to clipboard operation
app copied to clipboard

usage of oauth-scope-approval

Open dreamsbond opened this issue 7 years ago • 45 comments

APPROVAL_URI and ERROR_URI constant are pointed to "oauth-scope-approval"

i would like to know the usage implementing this route.

please help

dreamsbond avatar Sep 07 '18 01:09 dreamsbond

besides, i cannot find where the client_secret to set and get

please could you help?

dreamsbond avatar Sep 07 '18 09:09 dreamsbond

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.

neomerx avatar Sep 07 '18 10:09 neomerx

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.

neomerx avatar Sep 07 '18 10:09 neomerx

sorry. i found i was still confused about the setting in code grant basis on limoncello-app

to my understanding :

  1. i got 2 limoncello-app (A&B), A as a resource owner (http://localhost:8001), B as a authentication server (http://localhost)
  2. authorize via the http://localhost/authorize?client_id=default_client&redirect_url=http://localhost:8001
  3. get me to "APPROVAL_URI"
  4. 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.

dreamsbond avatar Sep 10 '18 03:09 dreamsbond

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?

dreamsbond avatar Sep 10 '18 12:09 dreamsbond

i search through the limoncello app and framework, still cannot locate the method for Authorization Response. can you help?

dreamsbond avatar Sep 11 '18 06:09 dreamsbond

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?

neomerx avatar Sep 11 '18 08:09 neomerx

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 avatar Sep 11 '18 11:09 dreamsbond

@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.

neomerx avatar Sep 12 '18 16:09 neomerx

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

image

image

i can then reached the redirect_uri callback.

and the authorization code was successfully written:

image

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:

image

dreamsbond avatar Sep 20 '18 06:09 dreamsbond

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.

neomerx avatar Sep 20 '18 16:09 neomerx

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.

dreamsbond avatar Sep 20 '18 17:09 dreamsbond

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.

neomerx avatar Sep 20 '18 18:09 neomerx

I have an initial version (very very beta really) of a server and a client.

If you run the server and login image

and then open the client in a separate tab image

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 image (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).

image

note the code is generated

neomerx avatar Sep 20 '18 21:09 neomerx

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.

dreamsbond avatar Sep 21 '18 02:09 dreamsbond

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.

dreamsbond avatar Sep 21 '18 02:09 dreamsbond

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

image

the code is here https://github.com/neomerx/demo-oauth-server

neomerx avatar Sep 21 '18 19:09 neomerx

The code has 2 commits

  • Initial commit (2 default Limoncello apps).
  • Server and client code added.

neomerx avatar Sep 21 '18 19:09 neomerx

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

dreamsbond avatar Sep 24 '18 02:09 dreamsbond

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.

neomerx avatar Oct 02 '18 18:10 neomerx

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.

dreamsbond avatar Oct 03 '18 10:10 dreamsbond

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...

dreamsbond avatar Oct 03 '18 10:10 dreamsbond

another question in mind.

the authorization code granted will be permanent or temporary?

dreamsbond avatar Oct 03 '18 10:10 dreamsbond

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?

  1. Get server base url (e.g. https://www.example.com) either from $_SERVER or from settings like
/** @var CacheSettingsProviderInterface $provider */
$provider  = $this->getContainer()->get(CacheSettingsProviderInterface::class);
$originUri = $provider->getApplicationConfiguration()[ApplicationConfigurationInterface::KEY_APP_ORIGIN_URI];
  1. Make an URL for login page
/** @var RouterInterface $router */
$router    = $this->getContainer()->get(RouterInterface::class);
$signInUrl = $router->get($originUri, AuthController::ROUTE_NAME_SIGN_IN);
  1. 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.

neomerx avatar Oct 03 '18 19:10 neomerx

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)

dreamsbond avatar Oct 04 '18 13:10 dreamsbond

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)

neomerx avatar Oct 04 '18 13:10 neomerx

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.

dreamsbond avatar Oct 04 '18 14:10 dreamsbond

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)).

neomerx avatar Oct 04 '18 20:10 neomerx

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?

dreamsbond avatar Oct 05 '18 12:10 dreamsbond

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.

neomerx avatar Oct 06 '18 16:10 neomerx