Open PyDevX opened 4 years ago
You need the latest version, 0.3.1
. https://github.com/flavors/django-graphql-jwt/pull/165 this includes checking for the refresh token in the cookie, so you don't need to send it.
i solve the problem ,
You are right when i use refreshToken mutation like that it works
mutation { refreshToken { token } }
but the actual problem is when set JWT_COOKIE_NAME and JWT_REFRESH_TOKEN_COOKIE_NAME settings other than default value mutation dont work.
Thanks
for now JWT_REUSE_REFRESH_TOKENS : True setting not work for JWT-refresh-token cookie it renews whenever token refreshed
Hello, I have the same question as @PyDevX and I'm not sure to understand how 0.3.1 solves this. I'm storing the token in cookies using the jwt_cookies() decorator. Using the settings from the doc for refresh tokens:
GRAPHQL_JWT = {
'JWT_VERIFY_EXPIRATION': True,
'JWT_LONG_RUNNING_REFRESH_TOKEN': True,
'JWT_EXPIRATION_DELTA': timedelta(minutes=5),
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7),
}
After 5 minutes the JWT expires and I cannot call the refresh mutation due to httpOnly cookie. Do I understand that django-graphql-jwt takes care of refreshing the token in the cookie the same way it takes care of adding the token in the cookie? In anycase after waiting JWT_EXPIRATION_DELTA, I have to authenticate again. So, how do you refresh the JWT in the cookie?
when you want to refresh token just call this mutation with no argument
mutation { refreshToken { token } }
then server side assign new cookies both for token and refrestoken
But my problem is now :
despite i set 'JWT_REUSE_REFRESH_TOKENS : True' setting still it gives new refreshToken
@PyDevX I understand that I can call mutation { refreshToken { token } }
without sending the token
at any time I want to refresh the current token. But I'm with @merodrem, how about I want to refresh the token and the expiration time has passed?
yet it is true
if your refreshToken is expired you wont be able to refresh token because it have been deleted when expired.
if your token is expired and your refrehToken cookie is still in your browser just send mutation. it bring new token to you
My problem is when i call refreshToken mutation , although 'JWT_REUSE_REFRESH_TOKENS : True' , it refresh refreshToken too..
I'm having absolutely no luck on this as well, and would appreciate some input. From what I can tell in the docs (but especially this issue), calling the authToken
mutation will get a JWT and expiry token, and add them to the cookies. Any future calls to refreshToken
(without args) should use the same cookie. In practice, not so much...I'm probably misunderstanding something, but the docs seem ambigous here (no clear examples)?
When I call the authToken
mutation (either in playground or app), the cookies are present in the response. However, immediate calls to refreshToken
(wihout input) do not have the same cookies, and result in the error Refresh token is required
. This was because the app and API were running on different ports (and different Docker containers. By proxying the app (via webpack) through the API itself I was able to remove this issue. Additionally, I no longer needed CORS or CSRF exemption.
It should also be noted that the RelayrefreshToken
mutation requires input, meaning it cannot be used in this way (not that it works for me anyway)...
GRAPHQL_JWT = {
"JWT_VERIFY": True,
"JWT_VERIFY_EXPIRATION": True,
"JWT_ALLOW_REFRESH": True,
"JWT_LONG_RUNNING_REFRESH_TOKEN": True,
}
urlpatterns = [
# Removed need for CSRF exemption by proxying app through API (with webpack and Docker network)
# path("graphql/", csrf_exempt(jwt_cookie(GraphQLView.as_view)))),
path("graphql/", jwt_cookie(GraphQLView.as_view))),
]
auth_token = graphql_jwt.ObtainJSONWebToken.Field()
verify_token = graphql_jwt.Verify.Field()
refresh_token = graphql_jwt.Refresh.Field()
# Long running refresh tokens
revoke_token = graphql_jwt.Revoke.Field()
delete_refresh_token_cookie = graphql_jwt.refresh_token.DeleteRefreshTokenCookie.Field()
I've been reading along with this guide (Hasura.io - best-practices-of-using-jwt-with-graphql) and it seems to agree on the use of JWTs (using cookies for refresh tokens, etc). However, I have been completely out of luck when trying to translate this into this package.
My thought process is that the user would login (with authToken
), which would return the JWT and attach the refresh token as a cookie. When the app loads again, it would first check for a refresh token (how, since it is httpOnly
?) or at least request a refreshed token using refreshToken
. However, since the cookie isn't present on that request it always returns that error (stating that token is required).
Any ideas?
Doing a bit more experimentation with this and wonder if it is because I am using two separate "domains" in development: port 3000
for the app and port 5000
for the api? I tried experimenting the the JWT Cookie Domain setting, but to no avail either.
UPDATE: I have negated this by proxying app requests (via Vue webpack proxy) to the API itself, via the Docker Compose network and container name. I have disabled CORS entirely and verified that I can access the API from the app still, so this shouldn't be the issue any more.
UPDATE 2: A bit more tweaking has shown that authenticating properly sets the cookies; however, I'm still trying to figure a few unexpected behaviours out.
I was able to get this working by fixing a few issues, but still think that the docs could be clarified to show an expected workflow for (especially the empty refreshToken
mutation).
docker-compose.yaml
(to allow better communication than default
network)target: "[container_name]:[api_port]"
)uri
to simply the API path (/graphql
) in developmentALLOWED_HOSTS
jwt_cookie
decorator to GraphQL (API) routeThe basic workflow involves a series of mutations at different points when authentication comes into play. When the app is loaded (ie. after refresh), the Viewer
query is the only query run initially (routes are disabled until loading is finished). If it fails, the refresh query kicks in (from Apollo error link). If that also fails, the app redirects to the sign in page (with a redirect url back for after authentication). If the refresh query succeeds, the Viewer
query is retried (via same Apollo error link) and the rest of the app is permitted to load and send its own queries. I also add the viewer to Vuex in the Viewer
query result callback, which lets the app know that the user is authenticated (for UI purposes).
authToken
mutation when signing (and refreshing window)refreshToken
via Apollo whenever a request fails because of authentication (then retrying operation if token refresh works)revokeToken
, deleteTokenCookie
and deleteRefreshTokenCookie
when logging out to clean up everythingNext steps would be immediately refreshing token when the app is refreshed (to get time to its expiry), then automatically refreshing before it would expire (to keep Apollo link from having to do so and slow an operation down).
@kendallroth Thanks for your great write ups.
Do you know how I can get the cookie to set in development when I'm using two different domains? I've added 'localhost' to Allowed hosts and set up CORs to allow 'localhost' as well.
However, even when my Response
object has the setCookie
attribute it doesn't set on localhost:3000 where my Next App is running.
I will confess that I am not overly familiar with either CORS or cookies, but can share what I remember. The important part that I remember is that, in order for HTTP only cookies to work, the app and server must be on the same domain. In local development, this means the same port as well (from what I can tell). Therefore, in development is it necessary to proxy the app's requests through the server itself somehow (example). So I'm not sure that two different domains would work with cookies unfortunately. In production, the app will typically be served from the same domain as the API (at least, that's how I will do it). Therefore, cookies will work by default (again, my understanding). However, should you use another domain (for example, serve the app with a CDN), I'm not sure how that would work (since it is two separate domains).
TL;DR: The app and server appear to need to be on the same domain in order for HTTP Only cookies to work.
Here's a link to my repository, see if following the code helps at all 😏 (GitLab - kendallroth/ourgroup
)
P.S. I welcome corrections from those more knowledgeable than myself; I am only speaking from my recent experience.
I have finally gotten to the stage of readying the app for production, and immediately ran headfirst into this issue. I am trying to serve the app from one subdomain and the API on another (same TLD). However, the JWT cookie (included in request after authentication) is not persisted with the other requests (it's broken...). I tried experimenting with JWT_COOKIE_DOMAIN
by setting it to the app subdomain, but no dice.
Is it not common to use JWT tokens via cookies on different domains, or does everyone serve the app from the same domain as the API?
I would appreciate guidance and documentation on this topic, as the limited resources right now do not really indicate how these cookies should be used (I understand that they are relatively new).
P.S. I have also had to re-enable CORS due to the different domains, and possible CSRF (appears to work so far?).
@mongkok Would you be able to lend some insight into how the JWT_COOKIE_DOMAIN
(and possibly JWT_COOKIE_SAMESITE
) are intended to be used? Do they need to be used together to allow cross domain cookies? Should setting JWT_COOKIE_DOMAIN
to the app's domain be enough to enable cross domain cookies between an app and server on different subdomains (but same domain)?
I can provide examples for the documentation if I get this working, but at the moment I am unfortunately stuck.
Any news about this? @kendallroth
No @psdon, I'm still hoping for some help or insight from the maintainers. It seems like there is support for the use case, but a lack of knowledge on my part or documentation here.
What's your current setup in production? You still create a proxy between the GraphQL API and your front-end?
I use Dokku on Digital Ocean to manage my app and server as two different "containers." The Python API is deployed relatively easily and run with gunicorn
, while the app is first build then served with a simple node
server script. The app and server containers use different subdomains (groups.
and groups-api.
respectively) of the same root domain.
I only proxy between the API and my app in local development (not production currently), as I understand that the different ports on localhost are treated as different hosts entirely?
// vue.config.js (Webpack override)
devServer: {
// Webpack proxy is used in devlopment to eliminate CORS by proxying requests
// through the API itself (using Docker network).
proxy: {
"^/graphql/": {
target: "http://groups-api:5000/",
changeOrigin: true,
},
},
}
When I try to log in on production, the request succeeds but and the cookie is set on the response. However, the cookie is not present on the next request/response, so it appears to be stripped off by the server.
# app/settings.py
DJANGO_ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",")
DJANGO_CORS_WHITELIST = os.getenv("DJANGO_CORS_WHITELIST", "").split(",")
JWT_APP_DOMAIN = os.getenv("JWT_APP_DOMAIN", None)
INSTALLED_APPS = [
# ...django apps
"graphene_django",
"graphql_jwt.refresh_token.apps.RefreshTokenConfig",
# NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied through
# a webpack proxy to the API itself!
"corsheaders",
"graphql_playground",
# ...my apps
]
MIDDLEWARE = [
# ...django middleware
"django.contrib.sessions.middleware.SessionMiddleware",
# NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied through
# a webpack proxy to the API itself!
# Apparently needs to be placed high in the list (above "CommonMiddleware")
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
# "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# Must be declared to allow non-graphql authentication
# 'graphql_jwt.middleware.JSONWebTokenMiddleware',
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
GRAPHENE = {
"SCHEMA": "app.schema.schema",
"MIDDLEWARE": ("graphql_jwt.middleware.JSONWebTokenMiddleware",),
}
ALLOWED_HOSTS = [
"localhost",
# Allow Docker containers IN DEVELOPMENT
"groups-api",
"groups-app",
] + DJANGO_ALLOWED_HOSTS
AUTHENTICATION_BACKENDS = [
"graphql_jwt.backends.JSONWebTokenBackend",
# Custom authentication backend is necessary to properly give errors in "tokenAuth"
# mutation, and custom sign in mutation cannot return refresh tokens...
"util.auth.GraphQLAuthBackend",
]
GRAPHQL_JWT = {
...
"JWT_COOKIE_DOMAIN": JWT_APP_DOMAIN,
# JWT token workflow
"JWT_VERIFY": True,
"JWT_EXPIRATION_DELTA": timedelta(minutes=15),
"JWT_VERIFY_EXPIRATION": True,
...
}
# NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied
# through a webpack proxy to the API itself!
CORS_ORIGIN_WHITELIST = DJANGO_CORS_WHITELIST
CORS_ALLOW_CREDENTIALS = True
Here's the repository if it gives any more insight into what may be happening (tried to summarize settings.py
above).
I am wondering if it is a misunderstanding of something on my part, as I thought that cookies could be passed across subdomains of the same root domain?
Why not just proxy the API and the front-end in the same domain? Using Nginx
On Sat, Sep 5, 2020, 11:49 PM Kendall Roth notifications@github.com wrote:
I use Dokku on Digital Ocean to manage my app and server as two different "containers." The Python API is deployed relatively easily and run with gunicorn, while the app is first build then served with a simple node server script. The app and server containers use different subdomains ( groups. and groups-api. respectively) of the same root domain.
I only proxy between the API and my app in local development (not production currently), as I understand that the different ports on localhost are treated as different hosts entirely?
// vue.config.js (Webpack override) devServer: { // Webpack proxy is used in devlopment to eliminate CORS by proxying requests // through the API itself (using Docker network). proxy: { "^/graphql/": { target: "http://groups-api:5000/", changeOrigin: true, }, },}
When I try to log in on production, the request succeeds but and the cookie is set on the response. However, the cookie is not present on the next request/response, so it appears to be stripped off by the server.
app/settings.pyDJANGO_ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",")DJANGO_CORS_WHITELIST = os.getenv("DJANGO_CORS_WHITELIST", "").split(",")
JWT_APP_DOMAIN = os.getenv("JWT_APP_DOMAIN", None) INSTALLED_APPS = [
...django apps
"graphene_django", "graphql_jwt.refresh_token.apps.RefreshTokenConfig", # NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied through # a webpack proxy to the API itself! "corsheaders", "graphql_playground", # ...my apps
] MIDDLEWARE = [
...django middleware
"django.contrib.sessions.middleware.SessionMiddleware", # NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied through # a webpack proxy to the API itself! # Apparently needs to be placed high in the list (above "CommonMiddleware") "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", # "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", # Must be declared to allow non-graphql authentication # 'graphql_jwt.middleware.JSONWebTokenMiddleware', "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] GRAPHENE = { "SCHEMA": "app.schema.schema", "MIDDLEWARE": ("graphql_jwt.middleware.JSONWebTokenMiddleware",), } ALLOWED_HOSTS = [ "localhost",
Allow Docker containers IN DEVELOPMENT
"groups-api", "groups-app",
] + DJANGO_ALLOWED_HOSTS AUTHENTICATION_BACKENDS = [ "graphql_jwt.backends.JSONWebTokenBackend",
Custom authentication backend is necessary to properly give errors in "tokenAuth"
# mutation, and custom sign in mutation cannot return refresh tokens... "util.auth.GraphQLAuthBackend",
] GRAPHQL_JWT = { ... "JWT_COOKIE_DOMAIN": JWT_APP_DOMAIN,
JWT token workflow
"JWT_VERIFY": True, "JWT_EXPIRATION_DELTA": timedelta(minutes=15), "JWT_VERIFY_EXPIRATION": True, ...
}
NOTE: CORS is currently not necessary IN DEVELOPMENT since app requests are proxied# through a webpack proxy to the API itself!CORS_ORIGIN_WHITELIST = DJANGO_CORS_WHITELISTCORS_ALLOW_CREDENTIALS = True
Here's the repository https://gitlab.com/kendallroth/ourgroup/-/blob/8-deploy-with-heroku/api/app/settings.py if it gives any more insight into what may be happening (tried to summarize settings.py above).
I am wondering if it is a misunderstanding of something on my part, as I thought that cookies could be passed across subdomains of the same root domain?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/flavors/django-graphql-jwt/issues/191#issuecomment-687628137, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHPURLAW2OMJFESQKBTGW7LSEJMZ5ANCNFSM4MGPKIIA .
That's a good question, with a two-part answer 😄. The first part is that I have never used Nginx for a proxy like this and am not quite sure where to start (time to bring out my Google-fu)... The second part is that I was assuming it would not be necessary given the JWT_COOKIE_DOMAIN
(and possibly JWT_COOKIE_SAMESITE
) configuration settings (and my semi-knowledge of cookies on subdomains).
So I'm using apollo client for my frontend and graphene for the backend. Here are a few of the configurations I had to do to get cookie-based authentication working. I'm also using django-cors-headers
apolloclient
const httpLink = createHttpLink({
uri: 'some-url-string',
credentials: 'include' // this is a must for cross-domain requests.
});
url.py
path('graphql/', jwt_cookie(csrf_exempt(GraphQLView.as_view(graphiql=True)))),
settings.py
CORS_ALLOW_CREDENTIALS = True # this must be included
In case anyone else runs into this. It took me many hours to understand what was really going on.
So if you use this configuration
GRAPHQL_JWT = {
'JWT_VERIFY_EXPIRATION': True,
'JWT_EXPIRATION_DELTA': timedelta(minutes=5),
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7),
}
You will have to refresh the token every 5 mins. In this case, you have to pass the token yourself to the refreshToken
query. But after 7 days any attempt to refresh it will fail. Which means you need to get a new one.
export const REFRESH_TOKEN = gql`
mutation RefreshToken($token: String!) {
refreshToken(token: $token) {
token
payload
refreshExpiresIn
}
}
`;
Also, you will not be able to query for the refreshToken
field in the token_auth
mutation.
If you instead opt for a long running refresh token by including 'JWT_LONG_RUNNING_REFRESH_TOKEN': True,
in your GRAPHQL_JWT
settings, you will have to include this additional line in your INSTALLED_APPS
and run python manage.py migrate
to create the database entries.
INSTALLED_APPS = [
...
'graphql_jwt.refresh_token.apps.RefreshTokenConfig',
...
]
This will also have the effect of setting the JWT-refresh-token
cookie for you. This means that you can call the refreshToken
mutation without passing the token
export const REFRESH_SILENTLY = gql`
mutation RefreshSilently {
refreshToken {
token
payload
refreshExpiresIn
}
}
`;
In this case, you can keep refreshing the refresh token (i.e replacing the old with the new) for as long as you want even though it has expired after 7 days. You then need to find a way to expire the token yourself and assign the user a new one. To automate this process, you could just run python manage.py cleartokens --expired
on a daily basis to remove expired tokens.
I'm open to correction on my current understanding.
Also in both cases you don't need to pass the authorization
header since the JWT
cookie is already set.
@chidimo I am currently trying to get the same setup going. Maybe this is a more general question, but if I use GraphiQL, should I see the Cookie also in Chrome dev tools set under Applicatoin-->Cookies? As mentioned in https://github.com/flavors/django-graphql-jwt/issues/210, I can also only see it in the setcookie response.
My settings look like this:
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
GRAPHQL_JWT = {
'JWT_COOKIE_SECURE': False if ENVIRONMENT == "dev" else True,
'JWT_COOKIE_DOMAIN': 'api.company.local',
'JWT_VERIFY_EXPIRATION': True,
'JWT_EXPIRATION_DELTA': timedelta(minutes=15),
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7)
}
@ptrhck first of all you, should definitely see set-cookie response headers when tokens are issued. Then, if cookie domain, path and other attributes are correct (match), you will see the cookies in Applicatoin-->Cookies of Chrome devtools.
@ivarsg Thank you! So this also holds true for GraphiQL?
On the other hand, asumming that the GraphQL API is at domain api.staging.company.com and the frontend with Apolllo client is at app.staging.company.com, do I need any domain settings or any other settings then the following?
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
GRAPHQL_JWT = {
'JWT_COOKIE_SECURE': False if ENVIRONMENT == "dev" else True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_EXPIRATION_DELTA': timedelta(minutes=15),
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7)
}
@ptrhck My setup works with the same settings as yours, on different subdomains.
In my case I had trouble getting it to work, but it turned out to be a setting in cloudfront which blocked cookies (origin request policy had to be set to Managed-AllViewer).
This is most likely not relevant to your situation, but posting it here anyway on the off chance it helps someone.
@syberen Thank you so much for your hint. It was an Nginx problem locally and an AWS problem in the cloud.
@kendallroth
Could you share your implementation how you have solved the following steps in Apollo?
@ptrhck While I never really resumed the project after setting it aside while seeking answers (due to project needs shifting), I think I have the answers you may be looking for. I've attached it here with some documentation I left, as it wasn't fully implemented (but I believe it was working to some extent)?
Call refreshToken
whenever a request fails because of authentication
// Error link for handling GraphQL errors
const errorLink = onError(
({ forward, graphQLErrors, networkError, operation }) => {
// if (response.errors && response.errors.length > 0) {
// TODO: Possibly re-enable this in the future to force mutation callback errors.
// Currently, Apollo treats anything with non-null 'data' as a success.
// However, data is set to an object even if an error occurred.
// response.data = null;
// }
if (graphQLErrors) {
for (const err of graphQLErrors) {
if (err && err.message) {
switch (err.message) {
// Handle missing refresh tokens (when trying to refresh a JWT)
case "Refresh token is required":
break;
// Retry queries/mutations after attempting to refetch a valid JWT
case "NOT_AUTHORIZED":
// TODO: After refreshing start countdown to next refresh (based on expiry)
// Taken from a variety of sources:
// - https://www.apollographql.com/docs/link/links/error/
// - https://github.com/apollographql/apollo-link/issues/646#issuecomment-423279220
return promiseToObservable(Auth.refresh()).flatMap(() => forward(operation));
}
}
}
}
...
}
);
Calling revokeToken
, deleteTokenCookie
, and deleteRefreshTokenCookie
when logging out
/**
* Signout the user
*
* Since JWT tokens (stored in cookies) are used for authentication,
* it is necessary to remove the JWT and refresh tokens.
*
* @param {function} cb - Optional callback
*/
static async signout(cb = null) {
Store.commit(`auth/${AUTH__AUTH_REMOVE}`);
Store.commit(`auth/${AUTH__REFRESH_TIMEOUT_REMOVE}`);
// NOTE: Operations are performed separately and in order of importance, in case one of them somehow fails.
// Revoke the refresh token
await Apollo.mutate({ mutation: RevokeTokenMutation });
// Delete the JWT cookie and refresh token
await Apollo.mutate({ mutation: DeleteRefreshTokenCookieMutation });
await Apollo.mutate({ mutation: DeleteTokenCookieMutation });
// Optional callback (router redirection, etc)
cb && cb();
}
Bonus points: auto-refreshing the JWT every so often
/**
* Refresh the current JWT (and trigger automatic refresh)
*/
static async refresh() {
const response = await Apollo.mutate({ mutation: RefreshTokenMutation });
if (!config.app.isProduction) console.info("JWT token was refreshed");
const expiryTime = response.data.refreshToken.payload.exp;
const currentTime = Date.now().valueOf() / 1000;
const timeToExpiry = Math.floor(expiryTime - currentTime);
// Set a timeout to automatically refresh the token before this one expires
Store.dispatch(`auth/${AUTH__REFRESH_TIMEOUT_SET}`, timeToExpiry);
}
Let me know if this is of any help
Is there a way to do logout in one mutation query rather than 3? It seems a bit excessive having to make 3 mutation calls (Revoke
, DeleteJSONWebTokenCookie
, DeleteRefreshTokenCookie
) just to logout properly =/
So I think I've figured out how to pass the cookie through Apollo using different ports (I'm using http://localhost:3000 for React with Apollo and http://localhost:8000 fro graphene-django). I have only been working on this a few months/weeks really so excuse my lack of full in-depth knowledge. I'm only a student at this but I've been knocking my head against the wall for a while with this.
The first thing I did was read like every single page of the documentation related to auth cookies and found out that there were a lot more features in django_graphql_jwt 0.3.1 so I ad to figure out how to install it and here's the relevant packages in myPipfile
:
graphene-django = "*"
django-cors-headers = "*"
pyjwt = "==1.7.0"
django-graphql-jwt = "==0.3.1"
So I played around with some of the new settings after looking at this page in the docs https://django-graphql-jwt.domake.io/en/latest/settings.html#cookie-authentication
And here is the relevant stuff in my settings.py
:
from datetime import timedelta
INSTALLED_APPS = [
'corsheaders',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django', # graphene
'items',
'profiles',
'rest_framework', # nevermind this, I'm not using it atm
'graphql_jwt.refresh_token.apps.RefreshTokenConfig' # refresh config
]
# Like I said I'm not using it I just copy pasted this
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
# my schema
GRAPHENE = {
'SCHEMA': 'myapp.schema.schema',
'MIDDLEWARE': [
'graphql_jwt.middleware.JSONWebTokenMiddleware'
]
}
GRAPHQL_JWT = {
'JWT_VERIFY_EXPIRATION': True,
'JWT_LONG_RUNNING_REFRESH_TOKEN': True,
'JWT_EXPIRATION_DELTA': timedelta(minutes=5),
'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7),
}
AUTH_USER_MODEL = 'profiles.Profile' # Have my own personal user type
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # put cors on top of course
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware'
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True # I think this is what allows it
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000" # I literally just wasn't sure if localhost was enough
]
AUTHENTICATION_BACKENDS = [
'graphql_jwt.backends.JSONWebTokenBackend',
'django.contrib.auth.backends.ModelBackend',
'myapp.backends.EmailBackend' # a backend I have to allow login via email
]
Now I just need to add the jwt_cookie
wrapper in the urls.py
from django.contrib import admin
from django.urls import path
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
from graphql_jwt.decorators import jwt_cookie
urlpatterns = [
path('admin/', admin.site.urls),
path('graphql/', jwt_cookie(csrf_exempt(GraphQLView.as_view(graphiql=True))))
]
And verify on localhost:8000 I get the cookies on tokenAuth
(I can't upload a pic)
So now I put this in my Apollo settings:
// This is just to query a variable I made in the client I'm notgoing to be using it later
const typeDefs = gql`
extend type Query {
isLoggedIn: Boolean!
}
`;
// Just print out the errors for now
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message }) => {
console.log(message);
});
}
if (networkError) {
console.log(networkError.message)
}
});
// make your link
const link = from([
errorLink,
new HttpLink({
uri: 'http://localhost:8000/graphql/',
credentials: 'include' // include credentials here
})
]);
const client = new ApolloClient({
link,
cache,
typeDefs,
});
And when I login like this
export const LOGIN_USER = gql`
mutation($username: String!, $password: String!) {
tokenAuth(username: $username, password: $password) {
payload
refreshExpiresIn
}
}
`;
const [loginUser, { loading }] = useMutation(LOGIN_USER, {
onError(err) {
err.graphQLErrors.forEach((errorObj) => {
if (errorObj.message.includes(INVALID_CREDENTIALS_ERROR)) {
setErrors({ ...errors, [INVALID_CREDS]: INVALID_CREDENTIALS_ERROR }); // setting custom errors
}
});
}
});
const authenticateUser = (e) => {
e.preventDefault();
loginUser({
variables: {
username: username.toLowerCase().trim() || email.toLowerCase().trim(),
password
}
}).then((res) => {
if (res.data) {
const { refreshExpiresIn, payload: { exp } } = res.data.tokenAuth; set only the expiry times in localStorage
context.login({ refreshExpiresIn, exp });
}
});
};
Hope this helps anyone because I was stuck for a WHIIIIIILE
I still have issues with this.
On my local, w/ django running in localhost:8000 and my frontend running at localhost:4000, i was able to hit TokenAuth and cookies are peristed.
However, on my dev env, django is in a subdomain django.mydomain.com
and frontend in another subdomain frontend.mydomain.com
and frontend does not persist the cookies.
Anyone know how to address this issue? i have no clue why the cookies are not being saved on frontend in my prod env but does in my local.
I used cookie based auth when i login successfully browser create two cookies : 1 for auth other is refresh due to HttpOnly , I cant access refresh token cookie from js and because of that i cant run refreshToken mutation with argument ? is there missing or wrong something that i miss?
You could also create a child class e.g MyTokenViewBase
from parent TokenViewBase
and override the post method to access the cookie from request.headers['Cookie']
instead of request.data
. Then modify the TokenRefreshView
to inherit from the child class, MyTokenViewBase
, and finally pass it into api/token/refresh
path
I used cookie based auth when i login successfully browser create two cookies : 1 for auth other is refresh due to HttpOnly , I cant access refresh token cookie from js and because of that i cant run refreshToken mutation with argument ? is there missing or wrong something that i miss?