-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
usage of oauth-scope-approval #55
Comments
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 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 |
sorry. i found i was still confused about the setting in code grant basis on limoncello-app to my understanding :
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 |
after a more deeper looked into the framework, i wrote a few lines to create authorization code as: 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. |
thanks @neomerx. it is helpful to me to learn how to cope with the code and flow. |
oh sorry, i didn't read your message properly. i just thought your update was there. |
Hi, I've added some code to the client and it seems to work (still very very beta). After you click the code is here https://github.com/neomerx/demo-oauth-server |
The code has 2 commits
|
i am currently following your demo, in the meantime, i looked into the method "setRedirectUriStrings()", for you information |
September is over and I finally have a better schedule. Back to |
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 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 So what you can do?
/** @var CacheSettingsProviderInterface $provider */
$provider = $this->getContainer()->get(CacheSettingsProviderInterface::class);
$originUri = $provider->getApplicationConfiguration()[ApplicationConfigurationInterface::KEY_APP_ORIGIN_URI];
/** @var RouterInterface $router */
$router = $this->getContainer()->get(RouterInterface::class);
$signInUrl = $router->get($originUri, AuthController::ROUTE_NAME_SIGN_IN);
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 |
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
|
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. |
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 |
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. |
let me try it out. |
Everything goes good except the part I do for accessing local resource. Is there any google/apiclient alike library which is not point to google? |
What do you mean by that? |
You mentioned about In case i am using google oauth, it is fine to use the library "google/apiclient". How about using the limoncello (client)? |
I meant you can receive user's email from limoncello server (either with the token or from a separate request) and then use this email to issue a local token. We did that with Google. When we got the user's email from Google it was used to check local Limoncello database and issue our own token for the user. |
will it risky though? |
The payload was verified by the google/apiclient, /** Controller handler */
const CALLABLE_GOOGLE_TOKEN_SIGN_IN = [self::class, 'googleTokenSignIn'];
public static function googleTokenSignIn(
/** @noinspection PhpUnusedParameterInspection */
array $routeParams,
ContainerInterface $container,
ServerRequestInterface $request
): ResponseInterface {
// you really **should** move it to settings
// it's hardcoded for simplicity and keeping focused on google authentication
$clientId = 'PUT-YOUR-GOOGLE-CLIENT-ID-HERE.apps.googleusercontent.com';
// request information about the user with the token
$idToken = $request->getParsedBody()['id_token'] ?? null;
if ($idToken !== null) {
$client = new \Google_Client(['client_id' => $clientId]);
$payload = $client->verifyIdToken($idToken);
} else {
$payload = false;
}
if ($payload !== false && (bool)$payload['email_verified'] === true) {
$userEmail = $payload['email'];
/** @var \App\Api\UsersApi $api */
$api = static::createApi($container, \App\Api\UsersApi::class);
$userId = $api->noAuthReadUserIdByEmail($userEmail);
if ($userId !== null) {
$shouldAuthCookie = true;
return static::authenticateUserById(
$userId,
$shouldAuthCookie,
$request->getQueryParams(),
static::getSettings($container, Authorization::class),
static::createRouteUrl($container, HomeController::ROUTE_NAME_HOME),
$container->get(PassportServerIntegrationInterface::class),
$container->get(CookieJarInterface::class)
);
}
}
// If we get here it's either invalid ID token of some other auth issue (e.g. no user with email from Google)
return new RedirectResponse(static::createRouteUrl($container, static::ROUTE_NAME_SIGN_IN));
} If in case i am not using google/apiclient, there will be a missing step on verifying the token and further to email verification. Am i correct? |
Of course, Google client works only with Google. |
This is callback method which will be called by Limoncello server. You will receive a token and any optional data. On the server side, you can add to the token any data such as email, name, phone number and whatever you wish. So getting user email might look like $userEmail = json_decode((string)$request)->user_email; |
This method adds custom properties to a token |
that is what i am confused. how could the callbacks knows the request was being verified if i am not using google/apiclient to verify. |
the google/apiclient did a check $client = new \Google_Client(['client_id' => $clientId]);
$payload = $client->verifyIdToken($idToken); |
I might need to create an example for that... |
i mean how callback know it is a valid request and not someone pretended to be a valid request |
Could u provide an example for limoncello app to limoncello app authorization code grant usage? |
APPROVAL_URI and ERROR_URI constant are pointed to "oauth-scope-approval"
i would like to know the usage implementing this route.
please help
The text was updated successfully, but these errors were encountered: