I am working on a project with the following architecture:
UI: React on client and server-side rendering via a Node server, Apollo Client for GraphQL,
API: Django handles GraphQL queries through Graphene.
I use Auth0 (JWT based) for my frontend authentication. I would like to use the token I get to authenticate my user in the context of the GraphQL queries API side.
[Edit2]
To pass the token to my API, I use:
const idToken = cookie.load('idToken') || null;
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
req.options.headers.authorization = `Bearer ${idToken}`;
next();
}
}]);
Then I need to retrieve it in Django: I use django-jwt-auth and the code proposed by #Craig Ambrose.
My authorization header is received and decoded (I can get the payload) but there is a problem when verifying the signature: I get "Error decoding signature."
This is strange since the signature is verified when I test it on jwt.io.
How can I authenticate on Django side ?
I've just done this using django-jwt-auth (not using Auth0)
That package provides a JSONWebTokenAuthMixin that you can combine with the GraphQLView from graphene_django, for example.
from jwt_auth.mixins import JSONWebTokenAuthMixin
class AuthGraphQLView(JSONWebTokenAuthMixin, GraphQLView):
pass
urlpatterns = [
url(r'^graphql', csrf_exempt(AuthGraphQLView.as_view(schema=schema))),
url(r'^graphiql', include('django_graphiql.urls')),
]
This works, but I found that graphiql stopped working, because it wasn't sending to token. I wanted to keep using cookie based auth for that, for dev purposes, so changed it to the following.
from jwt_auth.mixins import JSONWebTokenAuthMixin
class OptionalJWTMixin(JSONWebTokenAuthMixin):
def dispatch(self, request, *args, **kwargs):
auth = get_authorization_header(request)
if auth:
return super(OptionalJWTMixin, self).dispatch(request, *args, **kwargs)
else:
return super(JSONWebTokenAuthMixin, self).dispatch(request, *args, **kwargs)
class AuthGraphQLView(OptionalJWTMixin, GraphQLView):
pass
urlpatterns = [
url(r'^graphql', csrf_exempt(AuthGraphQLView.as_view(schema=schema))),
url(r'^graphiql', include('django_graphiql.urls')),
]
My setting is working now:
I have used code from #Craig Ambrose with django-jwt-auth. I had to fork the package on Github to handle the Audience 'aud' payload present in Auth0 Token.
def jwt_get_user_id_from_payload_handler(payload):
sub = payload.get('sub')
Auth0User = import_string('project.models.Auth0User')
auth0_user = Auth0User.objects.filter(auth0_id=sub)[0]
user_id = auth0_user.user.id
return user_id
JWT_PAYLOAD_GET_USER_ID_HANDLER = jwt_get_user_id_from_payload_handler
auth0_key = '<MyAuth0SECRET>'
JWT_SECRET_KEY = base64.b64decode(auth0_key.replace("_","/").replace("-","+"))
JWT_VERIFY = True
JWT_AUTH_HEADER_PREFIX = 'Bearer'
JWT_AUDIENCE = '<MyAuth0CLIENT_ID>'
With Aut0User a model with OnoToOne relation with classic Django user and a field with auth0_id.
Related
Building Microservices With Django and pyJWT
Let me explain this in summarised manor, maybe someone can help me out been at this for too long
I have three microservices i.e.
Authentication : Which creates the User and makes the token for a user using RS256 algorithm
Student : which is basically a user type that needs to be verified using JWT
Investor : which is also a user type that needs to be verified using JWT
[I have tried to make a generic create token function in authentication service something like]
data = {"id": user.uuid.hex, "type": user.user_type}
encoded = jwt.encode(data, private_key, algorithm="RS256")
return encoded
It is generating a correct token i have verified using JWT.io
In my student service i have created a middleware something like this
class JWTAuthenticationMiddleware(object):
#Simple JWT token based authentication.
#Clients should authenticate by passing the token key in the "Authorization"
#HTTP header, prepended with the string "Bearer ". For example:
# Authorization: Bearer <UNIQUE_TOKEN>
keyword = "Bearer"
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth = request.META.get("AUTHORIZATION", None)
splitted = auth.split() if auth else []
if len(splitted) == 2 and splitted[0] == self.keyword:
if not splitted or splitted[0].lower() != self.keyword.lower().encode():
return None
if len(splitted) == 0:
msg = _("Invalid token header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
elif len(splitted) > 2:
msg = _("Invalid token header. Token string should not contain spaces.")
raise exceptions.AuthenticationFailed(msg)
user = get_user(request)
decoded = verify_token(auth)
try:
student = Student.objects.get(user_id=decoded.get("id"))
except Student.DoesNotExist:
student = Student.objects.update(user_id=decoded.get("id"))
response = self.get_response(request)
return response
My verify_token function looks like
def verify_token(token):
decoded = jwt.decode(token, JWT_AUTH["PUBLIC_KEY"], algorithms=["RS256"])
return decoded
I have also added my middleware in my settings just below authentication middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"config.middleware.authentication.JWTAuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
I am just not able to decode my token and assign the payload to respective user type, Can someone help me out where i am going wrong? I have also tried using Authentication backend something like this, it doesn't works, have implemented simple_jwt_djangorestframework package but it doesn't decode my payload on student service and says invalid token so i don't want to add it for increasing unnecessary code too.
My viewset looks like this
class StudentViewset(viewsets.ViewSet):
queryset = Student.objects.filter(is_active=True)
serializer_class = StudentSerializer
lookup_field = "uuid"
permission_classes = [IsAuthenticated]
My error is always saying when i am using isAuthenticated as permission class
"message": "Authentication credentials were not provided.",
Maybe validating raw token should work :
decoded = verify_token(splitted[1])
Actually you don't have to implement it. There is simple jwt package for it. Install the package by its documentation and if it wouldn't work provide errors to help you.
To answer this question myself for future references
class JWTAuthenticationMiddleware(object):
#Simple JWT token based authentication.
#Clients should authenticate by passing the token key in the "Authorization"
#HTTP header, prepended with the string "Bearer ". For example:
# Authorization: Bearer <UNIQUE_TOKEN>
keyword = "Bearer"
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth = request.META.get("AUTHORIZATION", None)
splitted = auth.split() if auth else []
if len(splitted) == 2 and splitted[0] == self.keyword:
if not splitted or splitted[0].lower() != self.keyword.lower().encode():
return None
if len(splitted) == 0:
msg = _("Invalid token header. No credentials provided.")
return JsonResponse(data=msg, status=403, safe=False)
elif len(splitted) > 2:
msg = _("Invalid token header. Token string should not contain spaces.")
return JsonResponse(data=msg, status=403, safe=False)
user = get_user(request)
decoded = verify_token(auth)
try:
student = Student.objects.get(user_id=decoded.get("id"))
return self.get_response(request)
except Student.DoesNotExist:
msg = _("Invalid User Not found")
return JsonResponse(data=msg, status=403, safe=False)
couple of things that i was doing wrong was not passing a response when i found a student that's why middleware was not bypassed at that time, also exceptions cannot be used at middleware level middleware only returns HTTPResponses, thus i thought of using JsonResponse.
I'd like to call mutations from AppSync using my Python function but use a Cognito user for the authorization as "API-KEY", "IAM" and other methods are not suitable for my application.
My mutation looks like this (test purposes):
mutation XYZ {
updateTask(input: {id: "a1b2c3", name: "newTaskName"}) {
id
name
}
}
I am assuming that the user is already created and enabled by some means. If your AppSync API is secured only using Cognito, you are always going to need a username and a password to begin with. For example, you can use below code to login and get the AccessToken from the response:
import boto3
def get_user_auth(event, context):
client = boto3.client('cognito-idp')
response = client.initiate_auth(
UserPoolId='xxxxxxxxx',
ClientId='xxxxxxxxxxxxxx',
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': 'xxxxxx',
'PASSWORD': 'xxxxxx'
}
)
return response
Note: Make sure that you have "Enable username password based authentication (ALLOW_USER_PASSWORD_AUTH)" enabled.
Once you have the access token, you can use this in HTTP headers within your request as follows:
{
"authorization": "<YOUR-VERY-VERY-LONG-ACCESS-TOKEN>"
}
For example:
import requests
from requests_aws4auth import AWS4Auth
import boto3
session = requests.Session()
APPSYNC_API_ENDPOINT_URL = '<YOUR-API-URL>'
mutation = """mutation XYZ {updateTask(input: {id: "a1b2c3", name: "newTaskName"}) {id, name}}"""
response = session.request(
url=APPSYNC_API_ENDPOINT_URL,
method='POST',
headers={'authorization': '<YOUR-VERY-VERY-LONG-ACCESS-TOKEN>'},
json={'mutation': mutation}
)
print(response.json()['data'])
Since this access token has some expiration, you might also need to refresh this token by using the RefreshToken from the above response. Like so:
def refresh_token(self, username, refresh_token):
try:
return client.initiate_auth(
ClientId=self.client_id,
AuthFlow='REFRESH_TOKEN_AUTH',
AuthParameters={
'REFRESH_TOKEN': refresh_token,
# 'SECRET_HASH': self.get_secret_hash(username)
# If the User Pool has been defined with App Client secret,
# you will have to generate secret hash as well.
}
)
except botocore.exceptions.ClientError as e:
return e.response
Example of how you can generate secret hash.
So far, I was using API methods with functions
But now I am trying to build a class with every methods that have an API.
My main concern is about handling correctly the generation of the token. This is a basic authentification with a user/password i send in an xml with the /token resquest.
Once I generate an object, I want to get a token and refresh it when it expires. I want to avoid to get a token to each request I will make.
In the constructor, I put the user, password and root url of the API.
I also put self.token_access = get_token(self) with get_token the method to request a token.
If I put a refresh token before each request (one method by request), it will work. However, it will refresh the token (add time to the expiration date) but it would like to do so, only if the token is dead.
How would be the most elegant way to do so ?
Thank you a lot for your help
Please find some details of what I did so far below. This class is working, however, when I will build an API object,if the token expire, it won't manage the refresh of the token...
class API:
def __init__(self,host,user,password):
self.host = host
self.user = user
self.password = password
self.token = self.get_token()
def get_token(self):
xml = "<userCredentials><login>" +self.user+ "</login><password>" +self.password+ "</password></userCredentials>"
Response = requests.post(self.host + "tokens", data=xml, headers={"Content-Type": "application/xml"})
if Response.ok:
print(Response.content)
root = et.fromstring(Response.content)
token_session = root.find('tokenId').text
return token_session
else:
Response.raise_for_status()
def refresh_token(self):
try:
requests.put(self.host + "tokens" + "/" + self.token_access, data=xml, headers={"Content-Type": "application/xml"})
return self.token_access
except:
return self.get_token()
def methodap1_1(self):
...
I am successfully able to authorize my application with a 3rd party OAuth2 provider (Xero), but have been unable to refresh the token, either automatically, or manually.
The documentation suggests authlib can do this automatically. I have tried two different approaches from the Authlib documentation, on the flask client docs they give an example of "Auto Update Token via Signal", and on the web client docs they register an "update_token" function.
Using either approach, there is never an attempt made to refresh the token, the request is passed to Xero with the expired token, I receive an error, and the only way to continue is to manually re-authorize the application with Xero.
Here is the relevant code for the "update_token" method from the web client docs:
#this never ends up getting called.
def save_xero_token(name,token,refresh_token=None,access_token=None,tenant_id=None):
logging.info('Called save xero token.')
#removed irrelevant code that stores token in NDB here.
cache = Cache()
oauth = OAuth(app,cache=cache)
oauth.register(name='xero',
client_id = Meta.xero_consumer_client_id,
client_secret = Meta.xero_consumer_secret,
access_token_url = 'https://identity.xero.com/connect/token',
authorize_url = 'https://login.xero.com/identity/connect/authorize',
fetch_token = fetch_xero_token,
update_token = save_xero_token,
client_kwargs={'scope':' '.join(Meta.xero_oauth_scopes)},
)
xero_tenant_id = 'abcd-123-placeholder-for-stackoverflow'
url = 'https://api.xero.com/api.xro/2.0/Invoices/ABCD-123-PLACEHOLDER-FOR-STACKOVERFLOW'
headers = {'Xero-tenant-id':xero_tenant_id,'Accept':'application/json'}
response = oauth.xero.get(url,headers=headers) #works fine until token is expired.
I am storing my token in the following NDB model:
class OAuth2Token(ndb.Model):
name = ndb.StringProperty()
token_type = ndb.StringProperty()
access_token = ndb.StringProperty()
refresh_token = ndb.StringProperty()
expires_at = ndb.IntegerProperty()
xero_tenant_id = ndb.StringProperty()
def to_token(self):
return dict(
access_token=self.access_token,
token_type=self.token_type,
refresh_token=self.refresh_token,
expires_at=self.expires_at
)
For completeness, here's how I store the initial response from Xero (which works fine):
#app.route('/XeroOAuthRedirect')
def xeroOAuthLanding():
token = oauth.xero.authorize_access_token()
connections_response = oauth.xero.get('https://api.xero.com/connections')
connections = connections_response.json()
for tenant in connections:
print('saving first org, this app currently supports one xero org only.')
save_xero_token('xero',token,tenant_id=tenant['tenantId'])
return 'Authorized application with Xero'
How can I get automatic refreshing to work, and how can I manually trigger a refresh request when using the flask client, in the event automatic refreshing fails?
I believe I've found the problem here, and the root of it was the passing of a Cache (for temporary credential storage) when initializing OAuth:
cache = Cache()
oauth = OAuth(app,cache=cache)
When the cache is passed, it appears to preempt the update_token (and possibly fetch_token) parameters.
It should be simply:
oauth = OAuth(app)
oauth.register(name='xero',
client_id = Meta.xero_consumer_client_id,
client_secret = Meta.xero_consumer_secret,
access_token_url = 'https://identity.xero.com/connect/token',
authorize_url = 'https://login.xero.com/identity/connect/authorize',
fetch_token = fetch_xero_token,
update_token = save_xero_token,
client_kwargs={'scope':' '.join(Meta.xero_oauth_scopes)},
)
in addition, the parameters on my "save_xero_token" function needed to be adjusted to match the documentation, however this was not relevant to the original problem the question was addressing.
I am new to getStream.io and I am trying to understand a user creation flow with getstream.io and firebase. If I create a new user in firebase and then pass in their firebase UID to functions such as:
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET');
//generate new user
client.user('<FIREBASE UID>').create({name: "Jane Doe", occupation: "Software Engineer", gender: 'female'});
//generate token for the user
const userToken = client.createUserToken('<FIREBASE UID>');
//Allow user to follow a feed
timeline_feed_1.follow('user', '<FIREBASE UID>');
//Check followers for the user
<FIREBASE UID>.followers({limit: '10', offset: '10'});
Would this work or am I going about this all wrong?
Thank you for reading!
P.S I have looked at Users auth and profiles in getstream.io and just wanted to clarify that my firebase example is what was meant by "Stream is best used in combination with an application"
I implemented a Firebase + GetStream.io user creation flow and can share what I did.
Big picture: After creating a Firebase UID, you have to use your own backend server to connect with the Stream API to create a new user (use the Firebase UID as the user_id) and generate that user's JSON Web Token ("JWT"). Your backend server then passes this JWT to your front end client (Swift iOS in my case), which then uses this JWT to allow the user to connect to the Stream API and access his authorized feeds etc. I used Python runtime Google Cloud Functions with a HTTP trigger as my "backend server". My Swift code called these functions via an HTTP POST request.
Here is my Python code to create a Stream user, substitute your own API key and secret:
import stream
from flask import escape
def createStreamUser(request):
content_type = request.headers['content-type']
if content_type == 'application/json':
request_json = request.get_json(silent=True)
try:
id = request_json['id']
name = request_json['data']['name']
avatarURL = request_json['data']['avatarURL']
except:
raise ValueError("JSON is invalid, or missing a 'name' property")
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
userInfo = client.users.add(
id,
{"name": name},
get_or_create=True,
)
return
Here is a function which generates and returns a JWT to your front end client:
import stream
from flask import escape
def createUserToken(request):
content_type = request.headers['content-type']
if content_type == 'application/json':
request_json = request.get_json(silent=True)
try:
id = request_json['id']
name = request_json['data']['name']
except:
raise ValueError("JSON is invalid, or missing a 'name' property")
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
user_token = client.create_user_token(id)
return(user_token)
It looks like Stream is best used in combination with an application in the answer you referenced was about using Stream API on a server and authenticate users there and then provide your frontend code with a user token after successful authentication.
Stream API client initialised using user tokens has restricted access in terms of which feeds are accessible or writable.
It is not recommended to put API secret in your frontend code as it may lead to unauthorised access to other users' data if someone extracts it from your app.