laravel / passport

Laravel Passport provides OAuth2 server support to Laravel.
https://laravel.com/docs/passport
MIT License
3.29k stars 780 forks source link

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

Closed a8c71 closed 2 years ago

a8c71 commented 7 years ago

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?

JeanLucEsser commented 7 years ago

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 commented 7 years ago

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 commented 6 years ago

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

driesvints commented 6 years ago

Heya, thanks for sending this in. See my answer here for more info: https://github.com/laravel/passport/issues/839#issuecomment-429371578

I'll mark this as a feature request.

driesvints commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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.

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

@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 commented 5 years ago

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

lorisleiva commented 5 years ago

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:

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:

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 commented 5 years ago

@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 commented 3 years ago

any updates on this? @lorisleiva

driesvints commented 3 years ago

@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 commented 3 years ago

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 commented 2 years ago

Any update @lorisleiva

driesvints commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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

driesvints commented 2 years ago

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