I'm using Python Social Auth and Django OAuth Toolkit to manage my user accounts and restrict access to my REST API.
I can create a token for users that sign up manually with my app by using the regular
curl -X POST -d "grant_type=password&username=<user_name>&password=<password>" -u"<client_id>:<client_secret>" http://localhost:8000/o/token/
But when I register my users with PSA by their access token, I'd like to create a OAuth2 Toolkit token for my own app and return it as JSON to the client so it can use it for making requests with my API.
Presently, I generate token simply using generate_token from oauthlib, is that good practice? Should I take into consideration other factors?
from oauthlib.common import generate_token
...
#psa('social:complete')
def register_by_access_token(request, backend):
# This view expects an access_token GET parameter, if it's needed,
# request.backend and request.strategy will be loaded with the current
# backend and strategy.
token = request.GET.get('access_token')
user = request.backend.do_auth(token)
if user:
login(request, user)
app = Application.objects.get(name="myapp")
# We delete the old one
try:
old = AccessToken.objects.get(user=user, application=app)
except:
pass
else:
old.delete()
# We create a new one
tok = generate_token()
AccessToken.objects.get_or_create(user=user,
application=app,
expires=now() + timedelta(days=365),
token=tok)
return "OK" # I will eventually return JSON with the token
else:
return "ERROR"
I've recently used https://github.com/PhilipGarnero/django-rest-framework-social-oauth2 for this purpose like user Felix D. suggested. Below is my implementation:
class TokenHandler:
application = Application.objects.filter(name=APPLICATION_NAME).values('client_id', 'client_secret')
def handle_token(self, request):
"""
Gets the latest token (to access my API) and if it's expired, check to see if the social token has expired.
If the social token has expired, then the user must log back in to access the API. If it hasn't expired,
(my) token is refreshed.
"""
try:
token_list = AccessToken.objects.filter(user=request.user)\
.order_by('-id').values('token', 'expires')
if token_list[0]['expires'] < datetime.now(timezone.utc):
if not self.social_token_is_expired(request):
token = self.refresh_token(request)
else:
token = 'no_valid_token'
else:
token = token_list[0]['token']
except IndexError: # happens where there are no old tokens to check
token = self.convert_social_token(request)
except TypeError: # happens when an anonymous user attempts to get a token for the API
token = 'no_valid_token'
return token
def convert_social_token(self, request):
grant_type = 'convert_token'
client_id = self.application[0]['client_id']
client_secret = self.application[0]['client_secret']
try:
user_social_auth = request.user.social_auth.filter(user=request.user).values('provider', 'extra_data')[0]
backend = user_social_auth['provider']
token = user_social_auth['extra_data']['access_token']
url = get_base_url(request) + reverse('convert_token')
fields = {'client_id': client_id, 'client_secret': client_secret, 'grant_type': grant_type,
'backend': backend,
'token': token}
if backend == 'azuread-oauth2':
fields['id_token'] = user_social_auth['extra_data']['id_token']
response = requests.post(url, data=fields)
response_dict = json.loads(response.text)
except IndexError:
return {'error': 'You must use an OAuth account to access the API.'}
except UserSocialAuth.DoesNotExist:
return {'error': 'You must use an OAuth account to access the API.'}
return response_dict['access_token']
def refresh_token(self, request):
grant_type = 'refresh_token'
client_id = self.application[0]['client_id']
client_secret = self.application[0]['client_secret']
try:
refresh_token_object = RefreshToken.objects.filter(user=request.user).order_by('-id').values('token')[0]
token = refresh_token_object['token']
url = get_base_url(request) + reverse('token')
fields = {'client_id': client_id, 'client_secret': client_secret, 'grant_type': grant_type,
'refresh_token': token}
response = requests.post(url, data=fields)
response_dict = json.loads(response.text)
except RefreshToken.DoesNotExist:
return {'error': 'You must use an OAuth account to access the API.'}
return response_dict['access_token']
#staticmethod
def social_token_is_expired(request):
user_social_auth = UserSocialAuth.objects.filter(user=request.user).values('provider', 'extra_data')[0]
try:
return float(user_social_auth['extra_data']['expires_on']) <= datetime.now().timestamp()
except KeyError: # social API did not provide an expiration
return True # if our token is expired and social API did not provide a time, we do consider them expired
Related
I'm using Django 3.2 with the django.auth.contrib app and djangorestframework-jwt==1.11.0. How do I prolong/reissue a new session token upon receiving a request for an authenticated resource and validating the user can access that resource? I use the following serializer and view to login the user and issue the initial token
class UserLoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=255)
password = serializers.CharField(max_length=128, write_only=True)
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
username = data.get("username", None)
password = data.get("password", None)
user = authenticate(username=username, password=password)
if user is None:
raise serializers.ValidationError(
'A user with this email and password is not found.'
)
try:
payload = JWT_PAYLOAD_HANDLER(user)
jwt_token = JWT_ENCODE_HANDLER(payload)
update_last_login(None, user)
except User.DoesNotExist:
raise serializers.ValidationError(
'User with given email and password does not exists'
)
return {
'username':user.username,
'token': jwt_token
}
class UserLoginView(RetrieveAPIView):
permission_classes = (AllowAny,)
serializer_class = UserLoginSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
response = {
'success' : 'True',
'status code' : status.HTTP_200_OK,
'message': 'User logged in successfully',
'token' : serializer.data['token'],
}
status_code = status.HTTP_200_OK
return Response(response, status=status_code)
I have this in my settings file to keep the session to 1 hour initially
JWT_AUTH = {
# how long the original token is valid for
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=1),
}
The client submits the session token in the "Authorization" header and it is validated (for example) using the below view
class UserProfileView(RetrieveAPIView):
permission_classes = (IsAuthenticated,)
authentication_class = JSONWebTokenAuthentication
def get(self, request):
try:
token = get_authorization_header(request).decode('utf-8')
if token is None or token == "null" or token.strip() == "":
raise exceptions.AuthenticationFailed('Authorization Header or Token is missing on Request Headers')
decoded = jwt.decode(token, settings.SECRET_KEY)
username = decoded['username']
status_code = status.HTTP_200_OK
response = {
'success': 'true',
'status code': status_code,
'message': 'User profile fetched successfully',
'data': {
#...
}
}
except Exception as e:
status_code = status.HTTP_400_BAD_REQUEST
response = {
'success': 'false',
'status code': status.HTTP_400_BAD_REQUEST,
'message': 'User does not exists',
'error': str(e)
}
return Response(response, status=status_code)
What I would like to do in my response is send a new session token down to the user that is good for another hour but I'm unclear what call I need to make to generate such a token and/or edit/invalidate the existing one.
It is not possible to change a JWT after it is issued, so you can not extend its lifetime, but you can do something like this:
for every request client makes:
if JWT is expiring:
generate a new JWT and add it to the response
And the client will use this newly issued token.
for this, you can add a django middleware:
**EDITED
class ExtendJWTToResponse:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
jwt_token = get_authorization_header(request).decode('utf-8')
new_jwt_token = None
try:
payload = jwt.decode(jwt_token, settings.SECRET_KEY)
new_jwt_token = JWT_ENCODE_HANDLER(payload)
except PyJWTError:
pass
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
if new_jwt_token:
response['Refresh-Token'] = new_jwt_token
return response
And the client must check on 'Refresh-Token' header on response and if there is any it should replace the token and use the newly issued token with the extended lifetime.
note: it is better to throttle issuing new tokens, for example, every time the request token is going to expire in the next 20 minutes...
Firstly, I'd recommend to prefer djangorestframework-simplejwt over django-rest-framework-jwt (which is not maintained).
Both have these views basically:
Obtain token view (ie. login), takes credentials and returns a pair of access and refresh tokens
Refresh token view, takes a valid refresh token and returns a refreshed access token
You'll have 2 different lifetimes for your tokens. Your access token typically lives for a few minutes whereas your refresh token would stand as long as you'd like your session to be valid.
The access token is used to prove your authentication. When expired, you should request another one thanks to the refresh view. If your refresh token is not valid (expired or blacklisted), you can wipe the authentication state on your client and ask for credentials again to obtain a new pair.
By default, when you authenticate you'll have a refresh token valid until a fixed expiry. Once reached, even if you're active, you'll need to authenticate again.
If you need a slightly short lived session, you may want to mimic Django's SESSION_SAVE_EVERY_REQUEST to postpone the session's expiry. You can achieve this by rotating refresh tokens: when you request a new token to your refresh view, it will issue both renewed access and refresh tokens, and the refresh one would have its expiry postponed. This is covered by djangorestframework-simplejwt thanks to the ROTATE_REFRESH_TOKENS setting.
I have implemented sign up and login successfully with amazon cognito and fast api, but now I want to secure other endpoints with returned token of aws.
these are functions implemented so far:
optional_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login", auto_error=False)
async def get_current_user(*, db: Session = Depends(deps.get_db), token: str = Depends(optional_oauth2_scheme)):
try:
payload = jwt.decode(token, options={"verify_signature": False})
user = UserController.get_user_by_email(db, payload.get('username'))
except:
raise HTTPException(status_code=401, detail='Invalid username or password')
return user
def login(self, username, password, db: Session):
client = boto3.client('cognito-idp',region_name=os.getenv('COGNITO_REGION_NAME'))
try:
response = client.initiate_auth(
ClientId=os.getenv('COGNITO_USER_CLIENT_ID'),
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password
}
)
user = controllers.user.get_by_email(db, username)
if user:
user.access_token = response['AuthenticationResult']['AccessToken']
db.flush()
db.commit()
user = controllers.user.get_by_email(db, username)
return {"detail": {'data': user}}
else:
raise HTTPException(status_code=404, detail="That User doesn't exist")
now I want to use this function to logout endpoint and to have token to request headers.
Any help is appreciated
I have created a DRF api authenticated with jwt,the token is stored in a cookie.I can successfully access all the viewsets using the token with postman.It only becomes a problem when l want to pass the token to angular frontend for the same operations.I am using django rest framework backend and Angular 9 frontend.Also note that l am storing the token in a cookie.
My views.py
class LoginView(APIView):
def post(self,request):
#getting the inputs from frontend/postman
email =request.data['email']
password =request.data['password']
user=User.objects.filter(email=email).first()
#Authentication
if user is None:
raise AuthenticationFailed('User not found!')
if user.password!=password :
raise AuthenticationFailed("incorrect password")
payload = {
'id':user.id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=10),
'iat': datetime.datetime.utcnow()
}
token = jwt.encode(payload, 'secret', algorithm='HS256')
response = Response()
#storing the token in a cookie
response.set_cookie(key='jwt',value=token ,httponly=True)
response.data = {
'jwt':token
}
return response
class UserView(APIView):
def get(self,request):
token=request.COOKIES.get('jwt')
if not token:
raise AuthenticationFailed("unauthorised")
try:
payload =jwt.decode(token, 'secret', algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("session expired")
user=User.objects.get(id=payload['id'])
serializer=UserSerializer(user)
return Response(serializer.data)
class Update(APIView):
def get_object(self,request):
try:
token=request.COOKIES.get('jwt')
if not token:
raise AuthenticationFailed("unauthorised")
try:
payload =jwt.decode(token, 'secret', algorithms=['HS256'])
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("session expired")
user=User.objects.get(id=payload['id'])
return user
except User.DoesNotExist:
return Response("wakadhakwa",status=status.HTTP_204_NO_CONTENT)
def get(self,request):
obj=self.get_object(request)
serializer=UserSerializer(obj)
return Response(serializer.data)
def put(self,request):
obj=self.get_object(request)
serializer=UserSerializer(obj,data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response("corrupted data",status=status.HTTP_204_NO_CONTENT)
def delete(self,request):
all=self.get_object(request)
all.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Did you check that the cookie gets properly saved in browser when receiving response from login?
Are you calling the UserView endpoints from your Angular app with an AJAX call or are you reloading the page? If it is a call from the app make sure that the request sends cookies. It depends on how exactly you request the data, e.g. if you're using fetch, then make sure you have the option credentials: 'include' set. If you're requesting the data in some other way try to find in the documentation which option is used to enable sending credentials (cookies).
I am using (simple JWT rest framework) as a default AUTHENTICATION CLASSES
Now I want to write an API test case for one of my view which needed authentication
I don't know how to add "access" token and how to use this in rest framework test cases
I will be thankful if you answer to my question
You can do this using a rest_framework.APITestCase.
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)
Before that you need an access token which you can get from the API you are using to obtain a JWT access token. This is what I did in making test cases:
class BaseAPITestCase(APITestCase):
def get_token(self, email=None, password=None, access=True):
email = self.email if (email is None) else email
password = self.password if (password is None) else password
url = reverse("token_create") # path/url where of API where you get the access token
resp = self.client.post(
url, {"email": email, "password": password}, format="json"
)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertTrue("access" in resp.data)
self.assertTrue("refresh" in resp.data)
token = resp.data["access"] if access else resp.data["refresh"]
return token
def api_authentication(self, token=None):
token = self.token if (token is None) else token
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)
I'm using Django REST Framework and using this library to provide token based authentication to the frontend applications.
There is Login with Google implementation using django-allauth plugin.
I want to generate access token when user login using social account.
For handling social login and generating social account, I have created this view.
class GoogleLoginView(LoginView):
"""
Enable login using google
"""
adapter_class = GoogleOAuth2Adapter
serializer_class = CustomSocialLoginSerializer
def login(self):
self.user = self.serializer.validated_data['user']
self.token = TokenView().create_token_response(self.request)
return self.token
def post(self, request, *args, **kwargs):
self.request = request
self.serializer = self.get_serializer(
data=self.request.data,
context={'request': request}
)
self.serializer.is_valid(raise_exception=True)
url, header, body, status_ = self.login()
return Response(json.loads(body), status=status_)
The request data has user instance along with client_id and client_secret of the application.
But this gives error
'{"error": "unsupported_grant_type"}'
Version
django-oauth-toolkit==1.3.0
Got it solved by passing client_id and client_secret along with the social network access token and append other fields in the view like
def login(self):
self.user = self.serializer.validated_data['user']
# Store request
request = self.request
# Change request data to mutable
request.data._mutable = True
# Add required data to the request
request.data['grant_type'] = 'password' # Call Password-owned grant type
request.data['username'] = self.user.username # Fake request data to oauth-toolkit
request.data['password'] = '-' # Fake request data to oauth-toolkit
request.data['social_login'] = True # Important, if not set will use username, password
request.data['user'] = self.user # Important, assign user obj
# Change request data to non-mutable
request.data._mutable = False
# Generate token
self.token = TokenView().create_token_response(request)
return self.token