Replies: 22 comments 3 replies
-
|
So I actually got this to work with some simple changes, here is a small writeup for anyone that might be interested: First thing is to update the /token route: Notice that I need to Now we need to create a similar class to So this is the new class we need to replace it with: This is the same exact code for Your code now becomes: This works with /docs as well since the browser always sends back httonly cookies! |
Beta Was this translation helpful? Give feedback.
-
|
FYI - Blog series using this approach with FastAPI by Nils de Bruin - https://medium.com/@nils_29588 |
Beta Was this translation helpful? Give feedback.
-
|
In the future will this method be included in the docs? I think a httponly cookie is the best way to store the jwt token, and not sending in the body with json. Also thanks @joaodlf i will try your solution in my project in the next couple of days. Edit: Tried it today and it works nicely. |
Beta Was this translation helpful? Give feedback.
-
|
Isn't this approach still vulnerable to CSRF attacks? |
Beta Was this translation helpful? Give feedback.
-
|
@cbows yes it is still vulnerable to CSRF. But it's XSS safe (with httponly=true), which sending in the body (and storing on the client) isn't. SameSite is not fully supported in all browsers https://caniuse.com/#search=samesite yet. But i do think as well, that it does minimize the vulnerability a lot, if you don't have some cross-origin scenarios. Otherwise yes you have to use a token. in fastapi you could maybe implement it in your jwt claim and store it on the client. and with every request you send it in the header and compare it with the claim I think stuff like this would be awesome to include in the docs. fastapi and also the docs are really awesome. Thats why i think to have the best possible security in the docs is really nice and a big selling point :-) |
Beta Was this translation helpful? Give feedback.
-
|
I totally agree with you. This definitely should go in the docs. With SameSite=Lax it might even be superior to the current approach in terms of default security. |
Beta Was this translation helpful? Give feedback.
-
|
According to starlette docs, |
Beta Was this translation helpful? Give feedback.
-
|
I have to look if it's beeing set in my cookie. But that would make it even more relevant to include this in the docs or even implement a class in fastapi |
Beta Was this translation helpful? Give feedback.
-
|
@cbows I just checked my cookie and it does not set samesite. current version of fastapi (0.54.1) uses starlette (0.13.2). If you check starlette the current version is 0.13.4 and there it is set in the code. so fastapi first needs to update to 0.13.4 as a dependency. right now we have to set it manually |
Beta Was this translation helpful? Give feedback.
-
|
I agree the docs should be updated to an example with HttpOnly cookies since I think that's best practice to protect session tokens from XSS |
Beta Was this translation helpful? Give feedback.
-
|
Just as a heads up, you could follow the way that flask_jwt_extended does things where the function that sets the tokens as cookies also creates csrf tokens: |
Beta Was this translation helpful? Give feedback.
-
|
FYI, @wshayes broken link (I think) should lead to this: https://archive.is/UsaXo
Perhaps @SelfhostedPro's suggestion which shows:
And leads to: https://archive.is/Bv1ub |
Beta Was this translation helpful? Give feedback.
-
|
what is latest on this? |
Beta Was this translation helpful? Give feedback.
-
this page is now 404 |
Beta Was this translation helpful? Give feedback.
-
|
I think a better option is to save the token in a samtesite=Strict cookie
afaik this will prevent csrf and xss attacks |
Beta Was this translation helpful? Give feedback.
-
|
Would it be an alternative to stick with a short-lived access-token in the body and add an additional long-lived refresh-token in a cookie? |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Hi Joao, Many thanks for this code. It is working perfectly with my API. However, I wonder about one important issue. Whenever you try to log out using the interface from swagger (screenshot below), the cookie still keeps in the browser letting to access the secured endpoints you have. Consider these two cases below. In the first case, you can see that the endpoint "/api/v1/users/me" should not allow access because the user is not yet authenticated (this can be checked with the lock open). However because the cookie is still in the browser, you can retrieve the data from the endpoint anyway In this second case, I delete the cookie manually from my browser. After doing that, the behaviour is the one expected (if you try to get data from the endpoint when you are not authenticated you should receive a 401 response, Unauthorized) Wonder how this can be fixed in terms of whenever you logout using the Login/Logout Form you can add/destroy the cookie. Any ideas? |
Beta Was this translation helpful? Give feedback.
-
|
Hi @BigSamu, it's been quite a while since I've touched a project that uses fastapi, but your conclusion at the end makes sense to me - You'll need to hook into the logout endpoint (possibly via class extension as well?) and delete the cookie. |
Beta Was this translation helpful? Give feedback.
-
|
Is there a final solution on this ?? |
Beta Was this translation helpful? Give feedback.
-
https://swagger.io/docs/specification/v3_0/authentication/cookie-authentication/ According to the below link, the best way to make cookies work would be to authenticate via an endpoint using the cookie, and then navigate to the swagger UI page hosted on the same domain. You would lose any authentication UI elements in swagger, but that would work... |
Beta Was this translation helpful? Give feedback.
-
|
Having an A solid alternative is to use an access token for authentication and a refresh token for getting new access tokens. The access token lives in a tab's memory, which means it cannot be stolen using XSS. The flow is as follows:
This mitigates CSRF because the browser will have to actively set the value of the Authorization header in each request, which is simply impossible to "break" with a CSRF attack because JS is needed to manipulate the request's headers to insert And it mitigates XSS because the access token is stored in memory which means other tabs can't access it, unlike local storage for example. But keep in mind that implementing this in React will not be straight forward and for some reason there isn't a single decent tutorial out there on this. This is an example that implements a React Context doing what was just explained. import axios from "axios";
import {createContext, useContext, useEffect, useMemo, useState} from "react";
import {useNavigate} from "react-router-dom";
import {ROUTES} from "@/routes/index.jsx";
axios.defaults.baseURL = "https://p.local/api"
const AuthContext = createContext();
/**
* serializeToken takes the body of a successful authentication response
* containing an access token. In my backend setup for this project, the response body also
* includes an ISO timestamp indicating the token's expiration.
*
* The function returns an object that includes the token and its
* expiration information. This allows the frontend to:
* - Schedule automatic refreshes before the token expires
* - Know if the token is stale without having to test it on a protected endpoint
*/
export function serializeToken(data) {
const expiration = new Date(data.access_expiration)
return {
token: data.access,
expiration: expiration,
expirationPadded: new Date(expiration.getTime() - 2 * 1000) // -2 secs
}
}
export function isTokenValid(token) {
return token && token.expirationPadded > new Date()
}
function AuthProvider({children}) {
const [token, setToken] = useState();
const [error, setError] = useState();
const [user, setUser]=useState();
const navigate = useNavigate();
useEffect(() => {
refreshToken()
}, []);
async function refreshToken() {
try {
const response = await axios.post(
"/auth/token/refresh",
{},
{withCredentials: true}
)
const newToken = serializeToken(response.data)
setToken(newToken);
setError(null);
setUser(response.data.user)
return newToken
} catch (err) {
if (err.response) {
setError(err.response)
setToken(null)
}
// don't put navigate here. it will also force all public routes into /login
}
}
async function useFetch(method, url, data, config) {
let tokenToUse = token
if (!isTokenValid(token)) {
const newAccess = await refreshToken()
if (!newAccess) {
navigate(ROUTES.LOGIN.absPath)
return
}
tokenToUse = newAccess
}
if (tokenToUse) {
const response = await axios.request({
method,
url,
...(method === "get" ? { params: data } : { data }),
...config,
headers: {
Authorization: `Bearer ${tokenToUse.token}`,
...(config?.headers || {})
},
})
return response.data
}
}
const contextValue = useMemo(() => ({
token,
setToken,
error,
user,
setUser,
refreshToken,
useFetch,
}), [token, error, useFetch]);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
}
export const useAuthContext = () => {
return useContext(AuthContext);
};
export default AuthProvider;This can then be used to create a ProtectedRoute component: import {Navigate, Outlet} from "react-router-dom";
import {isTokenValid, useAuthContext} from "../store/authContext.jsx";
import LoadingPage from "@/components/pages/loadingPage.jsx";
import {ROUTES} from "@/routes/index.jsx";
export default function ProtectedRoute() {
const {token, error, refreshToken} = useAuthContext()
if (token) {
if (!isTokenValid(token)) {
refreshToken()
}
return <Outlet/>
}
if (error?.status === 401 || error?.status === 403) {
return <Navigate to={ROUTES.LOGIN.absPath}/>
}
return <LoadingPage/>
}You can check out my full code in this repo. You will probably have a hard time running the project since it's unfinished, but I polished pretty well everything auth related in that frontent. |
Beta Was this translation helpful? Give feedback.



Uh oh!
There was an error while loading. Please reload this page.
-
Description
First of all, I want to thank you for FastAPI - It's has been a while since I have been this excited about programming for the web. FastAPI is, so far, a really interesting project.
Looking through the documentation, I can see a very clear and concise practical guide to implement JWT tokens. I can see that the access token is returned as part of the response body:
This appears to be a requirement for the /docs to work as expected (where one can login and execute calls on the fly), this is really cool functionality, but it seems to be tied down to the response body.
I would like to be able to set a secure and httpOnly cookie to hold the access token, as I feel that exposing the access token as part of the response body is detrimental to the security of my application. At the same time, I would like the /docs to remain functional with a cookie based approach.
Would this be straightforward to accomplish? Is this at all supported out of the box by FastAPI?
Beta Was this translation helpful? Give feedback.
All reactions