Package: chowkidar-graphene
ChowkidarJWT Authentication for Django GrapheneAn JWT-based authentication package for DjangoGrapheneGraphQL APIs.FeaturesSupport forGrapheneGraphQL APIsToken & Refresh Token based JWT AuthenticationTokens stored as server-side cookieSupport for security measures - disable graphql introspection, get requests etc. on productionSupport for restricting 1 device / 1 login for a userSupport for logging IP & User-Agent of userAbility to Auto-Refresh JWT Token if the Refresh Token ExistsSupport for Social Auth via Google Identity ServiceSupport for Social Auth withsocial-app-djangoSupport for Authenticated GraphQL Subscriptions with Django ChannelsSupport for file uploads adhering toGraphQL Multipart Request SpecGet current logged-in user instance in info.context of resolversInstallationInstall the package -pipinstallchowkidar-grapheneAddchowkidartoINSTALLED_APPSand run migrations to apply required db changes to the database.Inurls.py, replace the GraphQLView coming from the Ariadne with the one from this packagefromchowkidar.graphqlimportGraphQLViewurlpatterns=[path('admin/',admin.site.urls),path('graphql/',GraphQLView.as_view(schema=schema),name='graphql'),...]Add the package to Graphene Middleware insettings.pyGRAPHENE={'SCHEMA':'framework.graphql.schema.schema','MIDDLEWARE':['chowkidar.auth.ChowkidarAuthMiddleware','graphene_django.debug.DjangoDebugMiddleware'],}Add the mutations to your schemafromchowkidar.graphqlimportAuthMutations,SocialAuthMutationsclassMutation(AuthMutations,SocialAuthMutations,...):passschema=graphene.Schema(mutation=Mutation,query=Query)Auth DecoratorsChowkidar supports 2 decorators that you can use in your API resolversfromchowkidar.graphqlimportlogin_required,fingerprint_required,resolve_user@login_requireddefresolve_field(self,info,sport=None,state=None,count=5,after=None):userID:str=info.context.userIDsome_function()@fingerprint_requireddefresolve_field(self,info,sport=None,state=None,count=5,after=None):refreshToken=info.context.refreshTokenuserID:str=info.context.userIDsome_function()@resolve_user()defmutate(self,info):user:User=info.context.useruserID:str=info.context.userIDsome_update()@login_required- checks if user is authenticated, and passes down his/her userID at
info.context.userID. Does not hit the db.@fingerprint_required- checks the refresh token of the user against db after validating its
fingerprint and returns info.context.userID & info.context.refreshToken@resolve_user- checks if user is authenticated, and passes down his/her instance at info.context.user
as well as his ID at info.context.userID. Hits the db to get the user instance.Both of these decorators, when wrapped around a query/mutation/type resolver, ensures that only logged-in users
can access it, when it is an unauthenticated request it shall
throw exception -PermissionDenied(message='User not authenticated', code='AUTHENTICATION_REQUIRED')GraphQL Subscriptions with Django ChannelsIn yourrouting.py-fromdjango.urlsimportpathfromchannels.routingimportURLRouter,ProtocolTypeRouterfromchowkidar.graphqlimportAuthenticatedChannelfrom.graphql.schemaimportschema# replace this with your schemaapplication=ProtocolTypeRouter({"websocket":(URLRouter([path("ws/",AuthenticatedChannel(schema,debug=True))])),})Doing this, you will get the logged-in user instance atinfo.context['user'],
which you can handle as per your wish. You can also use the@login_requireddecorator
that comes along with this package.MutationsOur Mutations (& GraphQL APIs) for auth may be not implemented according to basic graphql standards,
primarily because of technicalities involved in doing certain procedures (like setting up the cookie).Login a usersuccess, andid+usernameof the user are compulsory for the mutations to work.mutation{authenticateUser(email:"aswinshenoy65@gmail.com",password:"<MY_PASSWORD>"){successuser{idusername}}}note:user { id username }is a required selection for the authentication to happen.
Without id & username fields, it will not resolve & return 500.You may useusernamevariable to send email as well. However, you cannot do vice-versa.mutation{authenticateUser(username:"aswinshenoy65@gmail.com",password:"<MY_PASSWORD>"){successuser{idusername}}}on success the following is returned, and 3 cookies (REFRESH_TOKEN & ACCESS_TOKEN) are set -{"data":{"authenticateUser":{"success":true,"user":{"id":"2441264196336223233","username":"aswinshenoy"},"refreshExpiresIn":"2020-11-21T01:21:51.848Z"}}}Social Authmostly same asauthenticateUserbut sendsaccessToken+providerinstead ofemail/username+passwordmutation{socialAuth(accessToken:"<SOME TOKEN>",provider:"<SOCIAL_AUTH_PROVIDER>"){successuser{idusername}}}Google Authmostly same asauthenticateUserbut sendsaccessTokeninstead ofemail/username+passwordmutation{gAuth(accessToken:"<SOME TOKEN>"){successuser{idusername}}}Logout a userThis is required since the cookies are server-side, and needs to be removed.mutation{logoutUser}View sessions of a user{mySessions{isActivetokenuserAgentipissuedrevoked}}Revoke TokenRevoke a given refresh token of the usermutation($token:String!){revokeToken(token:$token)}Revoke Other TokensRevoke all tokens except the current token. Useful to logout user from all other devicesmutation{revokeOtherTokens}Available SettingsThe following are the settings variables for the plugin to be defined in your project'ssettings.py.
All the setting variables along with their defaults values are listed below -PROTECT_GRAPHQL=settings.DEBUGJWT_SECRET_KEY=settings.SECRET_KEYJWT_PUBLIC_KEY=NoneJWT_PRIVATE_KEY=NoneJWT_REFRESH_TOKEN_N_BYTES=20JWT_ALGORITHM=HS256JWT_EXPIRATION_DELTA=timedelta(seconds=60)JWT_REFRESH_TOKEN_EXPIRATION_DELTA=timedelta(seconds=60*60*24*7)JWT_LEEWAY=0JWT_ISSUER=NoneJWT_COOKIE_SAME_SITE='Lax'JWT_COOKIE_SECURE=FalseJWT_COOKIE_HTTP_ONLY=TrueJWT_COOKIE_DOMAIN='example.com'ENABLE_FINGERPRINTING=False# function with spec (user: User): bool, defaults to TrueALLOW_USER_TO_LOGIN_ON_AUTH='chowkidar.auth.rules.check_if_user_is_allowed_to_login'# function with spec (user: User): bool, defaults to FalseREVOKE_OTHER_TOKENS_ON_AUTH_FOR_USER='chowkidar.auth.rules.check_if_other_tokens_need_to_be_revoked'# function that gets called with auth object after a successful GAuthCHOWKIDAR_GAUTH_CALLBACK='chowkidar.auth.rules.handle_gauth'UPDATE_USER_LAST_LOGIN_ON_AUTH=TrueUPDATE_USER_LAST_LOGIN_ON_REFRESH=TrueUSER_GRAPHENE_OBJECT='user.graphql.types.user.PersonalProfile'LOG_USER_IP_IN_REFRESH_TOKEN=TrueLOG_USER_AGENT_IN_REFRESH_TOKEN=TrueGOOGLE_AUTH_CLIENT_ID='blah1blah2.apps.googleusercontent.com'FAQ1. How to know whether RefreshToken has expired in the frontend?When you do login using the API, we send backrefreshExpiresInon success.Create a local cookie with expire time set to thisrefreshExpiresIn, with some value.Before you send any authentication required requests (or before you open a auth required page), check whether this cookie exists.2. Do I need to refresh the access token?No, as long as you send both refresh token and the access token in the request (which you should automatically,
since its a server side cookie) the server will perform the refresh if a valid refresh token exists either when
approaching expiry of access token, or when having an expired access token. In both cases, the actual query shall
also be properly resolved and not failed. :)ContributingContributions are welcome! Feel free to open issues, and work on PRs to fix them.Building & Publishing the packagepythonsetup.pysdisttwineuploaddist/*CreditsThis project is heavily inspired bydjango-graphql-jwt&django-graphql-social-authby flavors,
and is loosely forked from its implementation.LicenseThis project is licensed under the GNU General Public License V3.