laravel / passport

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

Using passport with Cookies #734

Closed Tarasovych closed 5 years ago

Tarasovych commented 6 years ago

I'm using Passport with Cookies (instead storing JWT in LocalStorage). I have separate route for auth:

//api.php
Route::post('oauth/token', 'AccessTokenController@issueToken');

and overrided method, which sets Cookie:

class AccessTokenController extends \Laravel\Passport\Http\Controllers\AccessTokenController
{
    protected $cookie;

    public function issueToken(ServerRequestInterface $request)
    {
        return $this->withErrorHandling(function () use ($request) {
            $this->cookie = $this->convertResponse(
                $this->server->respondToAccessTokenRequest($request, new Psr7Response)
            );
            return $this->cookie;
        })
            ->cookie('laravel_token', json_decode($this->cookie->content())->access_token, 10, '/', '.nospampls.xyz', false, true);
    }
}

I don't have problems with getting Cookies on front-end app. But there is a problem with accessing auth api routes after authorization. Code on front-end:

let data = {
    grant_type: 'password',
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    username: this.email,
    password: this.password
}

axios.post('//api.mydomain.com/api/oauth/token', data, {
    withCredentials: true
}).then(response => {
    axios.get(process.env.API_BASE_URL + 'api/user', {
        withCredentials: true,
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        }
    })
    .then(response => {
        console.log(response)
    }).catch(error => {})
}).catch(error => {})

Request Cookie which is sent with GET api/user:

Cookie: laravel_token=<...>

And I get 401 Unauthorized for GET api/user. What's wrong? Cookie name is correct, according to this.

Tarasovych commented 6 years ago

If passport is looking for X-CSRF-TOKEN, how can I send it from server to Vue? Vue app i'm using isn't a part of Laravel, it's standalone.

Sephster commented 6 years ago

I probably would use the implicit grant instead of the password grant

Tarasovych commented 6 years ago

@Sephster passport's documentation is pretty little for easy understanding implicit grant (when you faced with oauth in 1st time). I've not also found some tutorials yet. Am I right with workflow below?

  1. User use his credentials for default login at Vue. Request is being sent to some (web or api?) Laravel route. Can I use default php artisan make:auth here? I think no, because if I want my standalone SPA to consume my API, all requests might be to api routes. Or I'm wrong?

  2. If success, user is being redirected to some page (can it be made by vue-side?) like vue.domain.com/oauth/authorize?<query>, where <query> has client_id, redirect_uri, response_type, scope.

  3. User approves auth and get redirected to redirect_uri (it can be somethink like vue.domain.com/dashboard or so - some route which might be protected by auth).

Thanks!

josh9060 commented 6 years ago

If the client is first party (e.g Spotify Web SPA) then use the password grant without the client secret (as its kind of pointless if you can right-click-view-source).

You should set the access token TTL to a length of an average user session and refresh the token just before it expires. This should be a good balance of security and UX.

With SPAs you shouldn't use a client secret for either password or implicit grants as there is no mechanism to keep the secret secure.

You should ensure your app has XSS and XSRF protection in place (e.g whitelisting static asset domains).

Finally, I would ask yourself am I using OAuth 2.0 for the sake of it - I find a lot of people use it for the sake of it when JWT is probably a better use case.

Tarasovych commented 6 years ago

@Joshgallagher I've got confused with auth types a bit. I have two apps: first is my own api (api.domain.com) - build with Laravel. Second is build with Vue. It's used for admin access (admin.domain.com) - it's first party app, I guess. So my choice is password or implicit grant?

When I was using password, I stored client_secret in app's env, so it wasn't right-click-view-source.

Sephster commented 6 years ago

JWT's are just self validating tokens. These can be used with OAuth 2. There is no spec that defines how your client gets a JWT to my knowledge. That is where OAuth 2 can come in to play. It is a mechanism to transfer tokens. They aren't the same thing so you can't advocate one for the other.

On 9 June 2018 14:31:45 BST, Joshua Gallagher notifications@github.com wrote:

If the client is first party (e.g Spotify Web SPA) then use the password grant without the client secret (as its kind of pointless if you can right-click-view-source).

You should set the access token TTL to a length of an average user session and refresh the token just before it expires. This should be a good balance of security and UX.

With SPAs you shouldn't use a client secret for either password or implicit grants as there is no mechanism to keep the secret secure.

You should ensure your app has XSS and XSRF protection in place (e.g whitelisting static asset domains).

-- You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub: https://github.com/laravel/passport/issues/734#issuecomment-395969598

-- Sent from my Android device with K-9 Mail. Please excuse my brevity.

josh9060 commented 6 years ago

When referring to JWT I meant this package: Tymon Jwt - should have elaborated.

Sephster commented 6 years ago

The thing devs need to take note of, as you've mentioned, is what are their requirements. If you want an authentication mechanism where you don't have to poll the DB for every login, the JWT Auth package you mention is fine.

However, I'm not sure it is a good use case for different clients accessing a protected resource as it doesn't really specify how the client should get an access token. As far as I can tell, you can login to the system that creates the JWT tokens and it will generate a JWT which you can then use for your API as well.

If you have a third party client that wants to access the API resource on behalf of a user, how do you get the JWT to that third party client without exposing the end users authentication details? This is the gap OAuth 2 fills.

If you have a third party client that you don't want to expose authentication details to for a protected resource, OAuth 2 is the industry standard and should be used. JWT is just a way to sign claims to transfer between two parties but the library you've mentioned doesn't follow any standard to obtain these tokens in the first place which is where the true difference lies.

Tarasovych commented 6 years ago

@Sephster as I've mentioned, I have my own (1st party app) which consumes again my own api. Now I'm looking for the suitable OAuth authentication flow. Passport offers password grant and implicit grant also. They are both suitable for my case, as for me...

I think I need to use JWT anyway, but out-of-package Passport send JWT as JSON, which can be stored in LocalStorage than. I've read that LocalStorage isn't as safe as Http Cookies are. So I used to write some logic which'll send JWT as Http Cookie to client.

Sephster commented 6 years ago

For an SPA you should use the Implicit grant with Passport. See here for more details.

Personally I would just stick with Passport's implementation for JWT storage as it will be secure enough

Tarasovych commented 6 years ago

@Sephster thanks. After thesre articles (one, two) I'm still thinking between Cookies and LocalStorage.

driesvints commented 5 years ago

Thanks for all the feedback provided @Sephster 👍

tcdsantos commented 5 years ago

I have Laravel Passport implemented in my project and it is everything working well except the cookie expiration time where the tokens are being stored (that is just 1 hour).

My project consists in a backend Laravel 5.8 api (with Laravel Passport) that serves a front SPA app (Vue).

Users from my app can login successfully using a page with a Vue component that makes a POST request with the user credentials and, if the login is successfully done, users are redirected to a new URL (app home) - this redirection is a GET request that creates the "laravel_token" cookie - created by the CreateFreshApiToken middleware.

From now on, users can go everywhere inside the app and all data needed from the app components' is obtained through ajax calls (Laravel will note the presence of the cookie "laravel_token" in these ajax calls and will identify the logged in user using the JWT present in that cookie).

My problem is:

The "laravel_token" cookie that was created when user logged in was created with a lifetime of just 1 hour. Because this is a SPA, this cookie never gets updated (exchanged by a new one, with a new hour lifetime)... so, after 1 hour, when a new ajax request needs to be done to the backend Laravel server, it will receive an Unauthenticated response - that makes sense because "laravel_token" cookie is outdated.

How do you deal with this problem?

I know that i can refresh this cookie by perfoming a full refresh/reload of the page before this cookie expire but this is not a good solution in terms of user experience.

I can't make an ajax call to refresh this cookie because this is a SPA and i don't have the client_id and it's secret from client side... and also because not only this cookie is httponly but also it is encrypted by Laravel - so, i can't exchange it by a new one using JS.

Is the only solution increase the lifetime of this cookie (from 1 hour to.... 1 year, for example)? Do you see any problem with this? And where can i set this cookie expiration time? Does i need to extend the ApiTokenCookieFactory class?

I would like user to be logged in until he deliberately performs a logout request or the access_token expires (that, in my case that i am using Laravel Passport defaults, is a long-lived token of 1 year).

I would appreciate if someone could help me with this problem.

If you see something that i am not doing the correct way, i also would appreciate your comments with suggestions.

Thank you very much!

aventrax commented 5 years ago

@tcdsantos I'm also using CreateFreshApiToken and it works, but I have kinda same issues as you got.

Firstly, the laravel scaffolding puts the CSRF token automatically on axios. Why? Shouldn't be CSRF useless on an API endpoint? How can I disable this? Disabling it on bootstrap.js results in 401 (unauthorized) on every request performed by axios! I can't get the reason why CSRF is requested using auth:api middleware! On AuthService provider the VerifyCsrfToken class is listed only on the web guard, not on the api guard!

Secondly, the laravel_token is sent me back with a cookie after the login+redirect. Than it is being sent back automatically whenever I use axios. All good, but I have the same problem you got: the cookie expiration is set on Session and I do not know why it hasn't the default passport token lifetime (1 year).

Many thanks to anybody who responds :=)

tcdsantos commented 5 years ago

@aventrax Thank you!

About your first question, Laravel puts the CSRF token automatically on axios because you can use it not only to make calls to your API routes (that don't need the CSRF token) but also to your web routes (and these will require CSRF token). I am not using axios... i am using jQuery but it is configured the same way: it always sends the CSRF token (even in requests to my API). The same happens with your cookies: they are all sent in every ajax call you make to the API.

For now, i created a timeout that makes a dummy ajax call (GET) every 10 minutes. This request is made to a web route that automatically (due to the CreateFreshApiToken middleware) refreshes all the cookies' expiration dates (XSRF-TOKEN, laravel_token and session). With this solution, everytime i make this ajax call i will extend the expiration of the cookies XSRF-TOKEN and laravel_token for a new hour long...

This is a workaround i found some people using but... it is a workaround and doesn't work if you put your computer to sleep and then wake it up 3 hours later (at this time, cookies will have expired and you can't refresh them because CSRF token is not valid anymore - and CreateFreshApiToken needs it to be valid to refresh them).

I am still looking for a better solution that will work even if you put your computer to sleep for 1 week... i want my users signed in for 1 year (without refreshing the browser... exactly as Facebook works).

aventrax commented 5 years ago

@tcdsantos supposing axios doesn't use CSRF header on API calls, why removing them results in a not-working calls (Unauthenticated)? My SPA, once loaded from laravel with initial values, uses only API calls and theese are not working without the CSRF header auto-added by laravel's bootstrap.js . That's what I don't understand, because the CreateFreshApiToken should auth the calls based on the laravel_token cookie. On the passport documentation on chapter "Consuming Your API With JavaScript", there is a point named "CSRF Protection" in which the first sentence is:

When using this method of authentication, the default Laravel JavaScript scaffolding instructs Axios to always send the X-CSRF-TOKEN and X-Requested-With headers.

"When using.." ? So, using CreateFreshApiToken I have a choice to use or not-use the CSRF, but I have no CsrfVerify middleware on my api guard, so it shouldn't be used, but without it, my SPA get 401 on any API call and the "laravel_token" cookie is present!

I don't understand =)

tcdsantos commented 5 years ago

@aventrax yes, you are right! I made some tests here and if i don't send the CSRF token when using API routes i algo get the Unauthenticated error.

I searched the web trying to figure out why is this happening (because API guard doesn't use the CreateFreshApiToken middleware) but i didn't find any answer - and Laravel official docs doesn't helped me too...

If you figure out where is the magic, please let us know... :)

nerg4l commented 5 years ago

@aventrax @tcdsantos You can disable the check for CSRF token with Passport::ignoreCsrfToken() but I would advise not to do that. Cookie authentication happens automatically so a malicious code could be able to send a request to your site from the client browser which would send the cookie as well.

Edit: Have you tried adding \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class to the list of api middlewares? I didn't check the implementation but I think that could work as well. Don't forget to add \App\Http\Middleware\EncryptCookies::class as well.

iBet7o commented 5 years ago

Hello,

Once you have logged in and the "laravel_token" cookie has been created, how does it validate that the user has logged in?

...
state const = {
    access_token: Cookies.get ('laravel_token') || ''
}
// Cookies.get ('laravel_token') returns undefined

const getters = {
    isAuthenticated: state => !! state.access_token
}
...

Regards,