Skip to content
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

Stateless access to Passport JSON API (using access_token) #379

Closed
a8c71 opened this issue May 17, 2017 · 28 comments
Closed

Stateless access to Passport JSON API (using access_token) #379

a8c71 opened this issue May 17, 2017 · 28 comments

Comments

@a8c71
Copy link

a8c71 commented May 17, 2017

I want Users using my API be able to create a new Client using the POST oauth/clients that uses by default web and auth middlewares.

Is there any way to specify the auth:api middleware to specific Passport routes to allow the use of access_tokens instead of web based authentication?

@a8c71 a8c71 changed the title Use access_token to access Passport JSON API Stateless access to Passport JSON API (using access_token) Jun 21, 2017
@JeanLucEsser
Copy link

Hi there, I have the exact same question.

Maybe I don't understand the way Passport should work, but it seems that the reason we want to use Passport is because our entire Authentication System is Stateless. So we shouldn't be redirected to a login route on our API server to access Passport JSON API.

We should be able to have an App responsible for asking our login credentials, asking Passport for an access_token, then use this token to manage our clients. Right?

Would love to hear back on the subject.

@tomcri
Copy link

tomcri commented Sep 3, 2017

Hello, I had the same issue guys, I spent 3 hours on that... All you have to do is to change the way web auth works. And in this case you need to:

'guards' => [
        'web' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],
    ],

in your config/auth.php

Or the other way is to modify Laravel Passport Routes manually and remove: Passport::routes(); from AuthServiceProvider.php.

As a best practice I think is better to not use Passport::routes() if you dont need all of that options (grants.. ).
In my case I use only a small part of laravel passport, and I created custom routes for Laravel Passport Controllers.

@nospoon
Copy link

nospoon commented Jul 11, 2018

If you change the web driver to passport then the api authentication will work, but frontend authentication will break.

@driesvints
Copy link
Member

Heya, thanks for sending this in. See my answer here for more info:
#839 (comment)

I'll mark this as a feature request.

@driesvints
Copy link
Member

Hey everyone. We're currently thinking of making some serious changes to the internals of Passport to make this happen. Basically all of the routes will become stateless and protected under the api and api:auth middlewares. This would mean that all of the functionality for setting cookies and checking CSRF could go away. This would also mean though that the CreateFreshApiToken middleware would need to be reimplemented as something like the way the crsf_token helper works.

Before we do this though I want to gather some opinions on this. What are your thoughts on this? Do you see any complications? Too big of an upgrade path?

Ping @Sephster

@JoaoPortella
Copy link

@driesvints I think it's the best way to make the tool more independent. But in my case I will not have to change anything, my app already is stateless

@matt-allan
Copy link
Contributor

Basically all of the routes will become stateless and protected under the api and api:auth middlewares.

@driesvints, could we make the middleware configurable? Some users have an existing server rendered app and want to add Passport for their api, other uses have a full SPA and want to add passport. This would make upgrading easier too.

Since the middleware is hardcoded in Laravel\Passport\RouteRegistrar it isn't really easy to configure that at the moment.

This would mean that all of the functionality for setting cookies and checking CSRF could go away.

Would we switch to using a real OAuth client and access tokens instead? That might help solve #909 too. Or is the plan to keep creating a 'transient' JWT and passing it as a header instead of as a cookie?

The benefit of using a cookie for your own app is you can set HTTP Only which makes it difficult to steal the JWT with a XSS attack. If we switch to using a bearer token it will be accessible to javascript.

@driesvints
Copy link
Member

I have to admit I don't have the time to look at this atm. Currently trying to focus on Cashier v10, sorry. I'll try to take a look at Passport after if I get to it. In the meantime you're always welcome to send a PR so Taylor can have a look.

@mtanmay
Copy link

mtanmay commented Jun 28, 2019

Any progress on this? I am in a situation where my SPA which is a standalone vue project, is isolated from the backend. I am unable to create clients because I can't provide the csrf_token from the SPA. At this moment I have to drop the entire passport package from my project and have to build my own auth mechanism. Please do something about it.

@lorisleiva
Copy link

Hi everyone 👋

I'm thinking of working on a PR for this. I'm making a SPA course so I've been digging into OAuth2 and Laravel Passport quite heavily recently and I was surprised to find out that Laravel Passport was basically only designed for server-rendered applications.

I also think like @matt-allan that the switch from stateful to stateless should be optional as it is an official Laravel plugin and it should work with "typical" Laravel applications.

One thing I love about Laravel Passport is how it supports both cookies and Bearer tokens out of the box and I think we should make sure to keep this flexibility for stateless APIs. I would also like to add that even httpOnly secure cookies are vulnerable to XSS.

if ($request->bearerToken()) {
    return $this->authenticateViaBearerToken($request);
} elseif ($request->cookie(Passport::cookie())) {
    return $this->authenticateViaCookie($request);
}

Aside from updating the middleware group, here are a few points that I foresee for this PR.

  • Update any code that assumes a SessionGuard is available.
  • When using cookies, ensure CSRF tokens are updated on each request. This should be fixed with Support get CSRF_TOKEN via cookie #515.
  • With more than one-legged flow (e.g. Authorization code grant) the passport::authorize page is not enough anymore since the user also needs to log in before reaching that page. In my opinion, this is the biggest complication. We don't want to force a make:auth since this will generate way more than needed. We should only need a small oauth/login page that authenticates the user and asks for its consent. Then a redirect_uri will be attached to the first response to the client so that it can redirect the user there.

Any suggestion before I get started would be more than appreciated.

P.S.: Here is a little table I've made of current solutions for token-based authentication with Laravel.

Screenshot 2019-07-18 at 10 18 54

@lorisleiva
Copy link

I've managed to make my last point work by adding an extra passport::login view when Laravel Passport is marked as stateless (in a Service Provider).

Passport::stateless();

Kapture 2019-07-18 at 13 44 57

However, what if users also want the "forgot password" option, etc. I'm starting to think it might make more sense to delegate to Auth::routes() and simply expect a server-rendered login page when using grants like the Authorization Code Grant. My only issue with that is that Single-Page Applications will typically end up with two login pages, one that is server-side rendered and one client-side rendered. Any thoughts on this?

@driesvints
Copy link
Member

@lorisleiva I'd just make Passport stateless by default tbh. I still have a picture in mind how I want to tackle this but probably won't have the time in the upcoming months. Feel free to send in a pr if you want.

@matt-allan
Copy link
Contributor

matt-allan commented Jul 18, 2019

Although the original issue was about making the JSON API endpoints stateless it sounds like now everyone is talking about making the authorize prompt and auth routes stateless too?

I Passport is stateless by default how would the authorization code grant work? Is the user expected to log back in every time a token needs to be granted?

Despite what the comment linked to above says, it's considered a best practice to only keep access tokens in memory:

The following measures should be used to protect access tokens:

  • Keep them in transient memory (accessible by the client application only).

https://tools.ietf.org/html/rfc6819#section-4.1.3
https://auth0.com/docs/security/store-tokens#don-t-store-tokens-in-local-storage
https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTML5_Security_Cheat_Sheet.md#local-storage

As a result, if you are writing a SPA and the user opens a new tab or refreshes the page you need to request a new access token. If you are using sessions on the authorization server it's not a big deal because the redirect happens immediately and the user is not prompted again. If you don't use sessions on the authorization server the user has to enter their username and password again, plus go through any 2 factor authentication prompts etc.

You also are not supposed to issue refresh tokens to browser apps and you should use a short expiration time for the access token, which means you need to go through the authorization flow a lot more often.

Edit: I can see the stateless setup being usable if you only support confidential clients (since they could have long lived access and refresh tokens) but is this a common enough use case that it makes sense for Passport to be stateless by default?

@lorisleiva
Copy link

@driesvints Thanks, I will do. Feel free to describe what you have in mind to make sure I implement something that’s more or less expected.

@matt-allan Yes, you make some very good points. First of all, the example I’ve shared above is actually using sessions to authenticate the user on the server side. So if the user clicks again on the login button, they will no longer need to authenticate. For all of the reasons you’ve mentioned, I also think it makes sense to keep that part stateful.

In my previous post I was asking whether we should assume the application scaffolded the entire authentication system using make:auth or if we should just provide a very simple login page for the user (prior to the authorize page). My reasoning behind this was that stateless APIs would typically not be using make:auth and would just create their own authentication pages in a SPA. I realise now that this does not make much sense since proper server-side authentication is needed for any grant type that includes redirections.

Regarding refresh tokens and public clients, I completely agree, they should not be used together. If you have a look at the table above, you’ll see that it’s basically one or the other but never both. I also think that the Password Grant (or any grant type that allow public clients) should not return any refresh token when no client secret is provided (and of course allow client secrets to be optional) but I think you’re waiting on oauth2-server for that.

One last thing, in the context of the course I’m building, I am trying the find a go-to secure solution for using Laravel Passport with SPAs. Would you mind if I DM you to run that solution pass through you?

@driesvints
Copy link
Member

@lorisleiva I was thinking of a way to entirely ditch the csrf requirements and let everything passport related go through API routes. But I don't have time atm to describe/think this over in detail, sorry.

@lorisleiva
Copy link

@driesvints No worries Dries that's already helpful. Let us worry about the details.

@lorisleiva
Copy link

I've been thinking about how to implement that PR a lot and I'd like to add a few points.

1. The easy bit
First of all, out of the 15 routes provided by Laravel Passport, 12 of them can easily become stateless by simply updating the middleware from ['web', 'auth'] to ['api', 'auth:api']. Instead of having some kind of Passport::middleware() option, I think it would be beneficial to expose the routes to the user to provide greater flexibility (more on that on point 4).

2. The authorization routes as-is
The 3 routes that are more delicate to make stateless are the following:

GET /oauth/authorize     # Shows the authorization page
POST /oauth/authorize    # Approves the authorization request
DELETE /oauth/authorize  # Denies the authorization request

These routes rely heavily on the web middleware group for the following reasons:

  • They expect the user to be automatically redirected to the /login page when not authenticated (hence assuming the use of make:auth).
  • They store an authRequest object in the session when the user is originally redirected and retrieve that authRequest object from the session when the user made a decision.

Here is a diagram illustrating the authorization process as-is. Orange pages are server-side rendered.

Laravel Passport-Page-1

3. Making authorization routes stateless
If we were to make those 3 authorization routes stateless, it would involve a complete rewrite of the authorization controllers. Since the pages will no longer be server-side rendered, most of its implementation will actually depend on how the client (typically a SPA) handles authorization on which we have absolutely no control.

Here is an attempt from me to illustrate what the authorization process could look like if the login and authorization pages lived in a SPA. Purple pages are client-side rendered (SPA).

Laravel Passport-Page-2

As you can see the process is much more complex and we can even end up having to generate a new token within the OAuth process simply to reach the authorize page.

Also, as @matt-allan mentioned above, without the use of sessions for these authorization routes, the user will end up having to enter their credentials again (plus any potential 2FA prompts) every time their token expires. Refresh tokens can be used to avoid that for confidential clients but not for public clients (e.g. SPAs, mobile applications, etc.).

As a result, my personal recommendation would be not to make these 3 routes stateless out-of-the-box since their implementation would rely heavily on a client on which we have no control. Instead, we need to allow developers to easily override these routes with their custom logic which brings us to the next point.

4. More control over routes
Instead of hiding the 15 routes provided by Laravel Passport in a RouteRegistrar, we could copy these routes to their Laravel application during passport::install in a new routes/passport.php file. The benefits of this approach would be:

  • Middleware groups can be edited directly in the routes file.
  • Developers have complete freedom over their OAuth routes. No need to set up route options like prefix to Laravel Passport.
  • Developers can very easily update the logic of a Laravel Passport controller by overriding it. This means developers can make authorization routes stateless by overriding the default controllers and implementing their custom logic.

5. CSRF and cookies
One last point that wasn't clear to me when I started using Laravel Passport is how it handles cookies. At first, I saw the TokenGuard checking for both bearer tokens and cookies and I just assumed that I could choose one or the other when exchanging tokens with clients. However, cookies are only checked to support TransientTokens generated by the CreateFreshApiToken middleware and the oauth/token/refresh route. More importantly, these tokens generated for cookies are never stored in the database and therefore cannot be revoked. Thanks @matt-allan for clarifying this for me.

Even though it might seem out of scope for this issue, I'm mentioning this here because there is a need to ditch the CSRF requirements and rethink the way the CreateFreshApiToken middleware works.

I think there is a need for cookie-based authentication beyond server-side rendered applications. We could add a cookie flag to the oauth_clients table that indicates whether or not we should attach access tokens to the cookies in addition to the usual flow. We could then use a default client flagged as cookie for storing transient tokens to have more control over them.

This would also be very useful for SPAs that simply want to authenticate their users with credentials and cookies. They could then use a Password Grant client flagged as cookie (that does not generate refresh tokens for public clients).

I'd love to have your thoughts on that before I start implementing a PR for point 4 and potentially another for point 5.

@matt-allan
Copy link
Contributor

@a8c71 could you explain how you would use this feature?

If the route to create a client is authenticated with an access token, but you need a client to create an access token, how do you get the access token?

It sounds like you might want dynamic client registration instead? (#804)?

@Robert-Hansen
Copy link

any updates on this? @lorisleiva

@driesvints
Copy link
Member

@Robert-Hansen if there aren't any new messages then no there aren't any updates. Please don't spam the issue tracker with that and instead try to help out here with a PR.

@IAndreaGiuseppe
Copy link

Hi, I made an improvement to solve this problem but nobody tells me how to share the PR. Can somebody tell me how/where I have to send the PR?

I simply move the routes outside the core to permit a good amount of configuration (like in all new laravel packages) and set up a Features class to handle the various sections

Thanks

@mdsohelmia
Copy link

Any update @lorisleiva

@driesvints
Copy link
Member

Was looking into this again. One piece of the puzzle that remains for me is the question asked by @matt-allan. If anyone could answer that we can maybe further look into this.

@a8c71
Copy link
Author

a8c71 commented Dec 27, 2021

Firstly, thank you all for the work done around this.

@a8c71 could you explain how you would use this feature?

If the route to create a client is authenticated with an access token, but you need a client to create an access token, how do you get the access token?

It sounds like you might want dynamic client registration instead? (#804)?

@matt-allan At the time we had several backend clients to our API, each one with their own Client credentials. One of them handled the user accounts and login flow (using SPA/access_tokens). With this particular client the users should be able to create their own Client credentials for their own backends and then authenticate with our API using Client Credentials Grant Tokens.

The approach suggested by @cristitoma should work for my case as my laravel project is backend only, but I see some points raised here in favor of decoupling other parts of the Passport JSON API. IMHO I dont mind some statefullness in exchange of better user experience and faster Passport integration; I'm in favor of @lorisleiva proposed approach.

@driesvints
Copy link
Member

@lorisleiva @a8c71 I realised I already did the work to move Passport's routes to a dedicated file for the upcoming v11 version: https://github.com/laravel/passport/blob/master/routes/web.php

Now that you have control over them as well as the middleware, does that mean that technically this issue is resolved when Passport v11 is released?

@lorisleiva
Copy link

Hi @driesvints 👋

This is not really fresh in my memory I'm afraid but from what I remember it, having control over these routes would definitely help create more custom authentication solutions. Maybe we could close this issue and reference it in different issues if users feel there are still gaps towards stateless authentication using Laravel Passport.

@a8c71
Copy link
Author

a8c71 commented Feb 2, 2022

@lorisleiva @driesvints feel free to close this issue as you please, thank you

@driesvints
Copy link
Member

Cool, we'll close this then. Thanks all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests