I’m building an authentication system using OAuth 2.0 with Authorization Code + PKCE flow. My setup includes two local development servers - one acting as the SSO identity provider and another serving the frontend application.
Here’s my current implementation:
The frontend app redirects users to get authorization, then processes the response at /login/redirect. During this step, I call a custom /token/exchange endpoint on my identity server to swap the auth code for tokens.
Since this happens in the browser, JavaScript can access the renewal token directly. I’m concerned about security implications here.
What are the recommended approaches for storing the renewal token securely? Should I keep it in memory, use httpOnly cookies, or is there a better pattern I should follow?
httpOnly cookies are defo your best bet! Avoid localStorage or sessionStorage for refresh tokens – that’s just asking for trouble. With httpOnly cookies, JavaScript can’t access them, which really helps against XSS. Just don’t forget to set secure and samesite flags for more safety.
I’ve had great success with in-memory storage and automatic token refresh in production. Store the refresh token in a closure or module variable - it survives the browser session but vanishes when users reload or close the tab. This completely eliminates XSS risks since the token never hits any storage that malicious scripts can access. The downside? Users have to re-authenticate after refreshing, but you can fix this with a sliding session that extends the access token based on activity. For the initial token exchange, try the Backend-for-Frontend pattern - your frontend server handles OAuth server-side, then creates its own session with the client. Keeps sensitive tokens away from browsers while users get a smooth experience.
Interesting setup! Are you handling token refresh automatically or doing it manually? Also, why’d you build a custom /token/exchange endpoint instead of using Laravel Passport’s built-in routes? That choce might impact which storage approach works best.