Django channel jwt custom middelware passing data - websocket

I'm trying to authenticate users using JWT through custom middleware for websocket communication. I'm sending the token from clientside by key, value and decode it from the serverside to get the user. But, when I try to access scope['user'] from comsumers, there is nothing. I want to know how I can pass on the scope['user'] to consumers. And I am not sure if this is a proper way to authenticate.
jwt_auth.py
#database_sync_to_async
def get_user(self, scope, decoded_data):
user = get_user_model().objects.get(id=decoded_data['user_id'])
return user
class JWTAuthMiddleware(BaseMiddleware):
def populate_scope(self, scope):
if b'token' not in scope['query_string']:
raise ValueError(
'JWTMiddleware cannot find token in scope.'
)
async def resolve_scope(self, scope):
token = parse_qs(scope['query_string'].decode('utf8'))['token'][0]
# print('token -> ' + str(token))
try:
UntypedToken(token) # Verify JWT
decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=['HS256'])
# print('decoded_data -> ' + str(decoded_data))
scope['user'] = await get_user(self, scope, decoded_data)
print('scope -> ' + str(scope))
# return self.inner(scope)
except (InvalidToken, TokenError) as e:
print(e)

Related

print(self.request.user.id) returns None

I have a Django REST App where I want to do the log in. I need the id of the currend logged in user and when I print it, it returns None. This is my code:
serializer.py
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(
label="Username",
write_only=True
)
password = serializers.CharField(
label="Password",
# This will be used when the DRF browsable API is enabled
style={'input_type': 'password'},
trim_whitespace=False,
write_only=True
)
view.py
class LoginAPI(APIView):
def post(self, request):
username = request.data['username']
password = request.data['password']
user = User.objects.filter(username=username).first()
if user is None:
raise AuthenticationFailed('User not found!')
if not user.check_password(password):
raise AuthenticationFailed('Incorrect password!')
payload = {
'id': user.id,
}
token = jwt.encode(payload, 'secret', algorithm='HS256').decode('utf-8')
response = Response()
response.set_cookie(key='token', value=token, httponly=True)
response.data = {
'token': token
}
print(self.request.user.id)
return response
I don't know what I have to change in my log in view in order to print the id, not None.
You didn't login yet in that post function. So self.request.user does not exist.
print(user.id)

Django OID token validation solutions

I am working on a Django rest-framework API. The frontend app (react) authenticates with a OpenID provider, and passes the authentication token to the API. I need to verify the authenticity of the token before serving the client requests.
As far as I understand, OID libraries that I have seen give a client and a provider implementation, but the above scenario seems to not be covered, i.e the API is neither a client, or a provider.
I have found one source describing the validation steps required, but I was wondering if there is an opensource solution that I have not found yet that will perform validation of the token for me.
Update
This is the implementation I have made:
import requests
import datetime
import six
from logging import getLogger
from requests.auth import HTTPBasicAuth
from urllib.parse import urljoin
from requests.exceptions import HTTPError
from calendar import timegm
from jwkest import JWKESTException
from jwkest.jwk import KEYS
from jwkest.jws import JWS
from django.utils.encoding import smart_text
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.conf import settings
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from .utils import class_cache
logger = getLogger(__name__)
def setting(key):
return getattr(settings, f'OIDC_AUTH_{key}')
class AuthenticatedServiceClient:
def __init__(self, roles, user_id):
self.roles = roles
self.id = user_id
def is_authenticated(self):
return True
#staticmethod
def create(payload, request):
roles = payload.get('role', [])
user_id = payload.get('sub', None) # framework specific code
if not user_id:
msg = _('No sub claims provided')
logger.error(msg)
raise AuthenticationFailed(msg)
return AuthenticatedServiceClient(roles, user_id)
class NoAuthentication(BaseAuthentication):
def authenticate(self, request):
no_user_id = '00000000-0000-0000-0000-000000000000'
return AuthenticatedServiceClient.create({'sub': no_user_id}, request), True
class BaseOidcAuthentication(BaseAuthentication):
#cached_property
def oidc_config(self):
url = urljoin(setting('OIDC_ENDPOINT'), '.well-known/openid-configuration')
return requests.get(url, verify=setting("SSL_VERIFY")).json()
class BearerTokenAuthentication(BaseOidcAuthentication):
www_authenticate_realm = 'api'
def authenticate(self, request):
bearer_token = self.get_bearer_token(request)
if bearer_token is None:
return None
try:
token_info = self.introspect_token(bearer_token)
except HTTPError:
msg = _('Invalid Authorization header. Unable to verify bearer token')
logger.error(msg)
raise AuthenticationFailed(msg)
logger.debug(f"Received token: {token_info}")
self.validate_bearer_token(token_info)
return AuthenticatedServiceClient.create(token_info, request), True
def validate_bearer_token(self, token_info):
if token_info['active'] is False:
msg = _('Authentication Failed. Received Inactive Token')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting('OIDC_SCOPE') not in token_info['scope']:
msg = _('Authentication Failed. Invalid token scope')
logger.error(msg)
raise AuthenticationFailed(msg)
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if utc_timestamp > int(token_info.get('exp', 0)):
msg = _('Invalid Authorization header. Bearer token has expired.')
logger.error(msg)
raise AuthenticationFailed(msg)
def get_bearer_token(self, request):
auth = get_authorization_header(request).split()
auth_header_prefix = setting('BEARER_AUTH_HEADER_PREFIX').lower()
if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
msg = _('Authorization failed. No bearer token found in header')
logger.error(msg)
return None
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided')
logger.error(msg)
raise AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string should not contain spaces.')
logger.error(msg)
raise AuthenticationFailed(msg)
elif smart_text(auth[1]).count('.') == 2:
msg = _('Authorization failed. Unexpected token format')
logger.error(msg)
return None
return auth[1]
#class_cache(ttl=setting('BEARER_TOKEN_EXPIRATION_TIME'))
def introspect_token(self, token):
response = requests.post(
self.oidc_config['introspection_endpoint'],
auth=HTTPBasicAuth(setting('OIDC_SCOPE'), setting('OIDC_INTROSPECT_PASSWORD')),
data={'token': token.decode('ascii')},
verify=setting("SSL_VERIFY"))
return response.json()
class JSONWebTokenAuthentication(BaseOidcAuthentication):
"""Token based authentication using the JSON Web Token standard"""
www_authenticate_realm = 'api'
def authenticate(self, request):
jwt_value = self.get_jwt_value(request)
if jwt_value is None:
return None
payload = self.decode_jwt(jwt_value)
logger.debug(f"Received token: {payload}")
self.validate_claims(payload)
return AuthenticatedServiceClient.create(payload, request), True
def get_jwt_value(self, request):
auth = get_authorization_header(request).split()
auth_header_prefix = setting('BEARER_AUTH_HEADER_PREFIX').lower()
if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
return None
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided')
logger.error(msg)
raise AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string should not contain spaces.')
logger.error(msg)
raise AuthenticationFailed(msg)
elif smart_text(auth[1]).count('.') != 2:
return None
return auth[1]
def jwks(self):
keys = KEYS()
keys.load_from_url(self.oidc_config['jwks_uri'], verify=setting("SSL_VERIFY"))
return keys
#cached_property
def issuer(self):
return self.oidc_config['issuer']
#class_cache(ttl=setting('JWKS_EXPIRATION_TIME'))
def decode_jwt(self, jwt_value):
keys = self.jwks()
try:
id_token = JWS().verify_compact(jwt_value, keys=keys)
except JWKESTException:
msg = _('Invalid Authorization header. JWT Signature verification failed.')
logger.error(msg)
raise AuthenticationFailed(msg)
except UnicodeDecodeError:
msg = _('Bad token format. Token decoding failed.')
logger.error(msg)
raise AuthenticationFailed(msg)
return id_token
def get_audiences(self, id_token):
return setting('AUDIENCES')
def validate_claims(self, id_token):
if isinstance(id_token.get('aud'), six.string_types):
# Support for multiple audiences
id_token['aud'] = [id_token['aud']]
if id_token.get('iss') != self.issuer:
msg = _('Invalid Authorization header. Invalid JWT issuer.')
logger.error(msg)
raise AuthenticationFailed(msg)
if not any(aud in self.get_audiences(id_token) for aud in id_token.get('aud', [])):
msg = _('Invalid Authorization header. Invalid JWT audience.')
logger.error(msg)
raise AuthenticationFailed(msg)
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if utc_timestamp > id_token.get('exp', 0):
msg = _('Invalid Authorization header. JWT has expired.')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting("ENABLE_NBF_CHECK") and 'nbf' in id_token and utc_timestamp < id_token['nbf']:
msg = _('Invalid Authorization header. JWT not yet valid.')
logger.error(msg)
raise AuthenticationFailed(msg)
if 'iat' in id_token and utc_timestamp > id_token['iat'] + setting('LEEWAY'):
msg = _('Invalid Authorization header. JWT too old.')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting('OIDC_SCOPE') not in id_token.get('scope'):
msg = _('Invalid Authorization header. Invalid JWT scope.')
logger.error(msg)
raise AuthenticationFailed(msg)
def authenticate_header(self, request):
return 'JWT realm="{0}"'.format(self.www_authenticate_real
At this time it doesn't seem like there is an official library supporting it.
I have put together the following implementation, using https://github.com/ByteInternet/drf-oidc-auth as a base:
https://gist.github.com/latusaki/0f015643d55c2481bb7acd023c4203e3

How to have my google oauth only try and authorize the app on a certain route

I have a google Oauth that will make the user authorize when a user goes to my webpage, however I only want them to have to authorize the app so that I can get their access and refresh tokens when they go to certain page to enter a google api information.Google is making them authorize no matter what route they are on any ideas on how to stop this.Ruby wont let me any of this in a route.
def user_credentials
# Build a per-request oauth credential based on token stored in
session
# which allows us to use a shared API client.
#authorization ||= (
auth = settings.authorization.dup
auth.redirect_uri = to('/oauth2callback')
auth.update_token!(session)
auth
)
end
configure do
Google::Apis::ClientOptions.default.application_name = 'Get Login
info for Google Ad Exchange'
Google::Apis::ClientOptions.default.application_version = '1.0.0'
client_secrets = Google::APIClient::ClientSecrets.load
authorization = client_secrets.to_authorization
authorization.scope =
'https://www.googleapis.com/auth/adexchange.seller.readonly'
set :authorization, authorization
end
before do
# Ensure user has authorized the app
unless user_credentials.access_token || request.path_info =~
/^\/oauth2/
redirect to('/oauth2authorize')
end
end
after do
# Serialize the access/refresh token to the session and credential
store.
# We could potentially need to pull back the client_id and
client_secret as well and add them to the dynamo database.
# session[:client_id] = user_credentials.client_id
# session[:client_secret] = user_credentials.client_secret
session[:access_token] = user_credentials.access_token
session[:refresh_token] = user_credentials.refresh_token
session[:expires_in] = user_credentials.expires_in
session[:issued_at] = user_credentials.issued_at
end
get '/oauth2authorize' do
# Request authorization
redirect user_credentials.authorization_uri.to_s, 303
end
get '/oauth2callback' do
# Exchange token
user_credentials.code = params[:code] if params[:code]
user_credentials.fetch_access_token!
redirect to('/')
end
Figured it out, meant to post way earlier but had an alert on this post so i figured id update to what we did to ake it work.
get '/googleauth' do
salesforce_username = params[:salesforce_username] || ''
unless session.has_key?(:credentials)
redirect to('/oauth2callback')
end
client_opts = JSON.parse(session[:credentials])
auth_client = Signet::OAuth2::Client.new(client_opts)
redirect to('/googleadx')
end
get '/oauth2callback' do
client_secrets = Google::APIClient::ClientSecrets.load
auth_client = client_secrets.to_authorization
auth_client.update!(
:scope => 'https://www.googleapis.com/auth/adexchange.seller.readonly',
:redirect_uri => url('/oauth2callback'))
if request['code'] == nil
auth_uri = auth_client.authorization_uri.to_s
redirect to(auth_uri)
else
auth_client.code = request['code']
auth_client.fetch_access_token!
session[:access_token] = auth_client.access_token
session[:refresh_token] = auth_client.refresh_token
session[:expires_in] = auth_client.expires_in
session[:issued_at] = auth_client.issued_at
auth_client.client_secret = nil
session[:credentials] = auth_client.to_json
redirect to('/googleadx')
end
end
get '/googleadx' do
# configure()
if params[:username]
successmessage = params[:username] + "'s credentials added successfully."
else
message = ''
end
salesforce_username = session[:salesforce_username] || ''
access_token = session[:access_token]
refresh_token = session[:refresh_token]
googleDollarLimit = ''
erb :googleadx, locals: {message: message, successmessage: successmessage, salesforce_username: salesforce_username, access_token: access_token, refresh_token: refresh_token, googleDollarLimit: googleDollarLimit}
end

Django token authorization - customizing obtain_jwt_token by overriding ObtainAuthToken class

My goal is to override obtain_jwt_token in order to get more control over the return value of the procedure, and in the doc I found only a bizarre and sketchy info on how to do this:
Note that the default obtain_auth_token view explicitly uses JSON
requests and responses, rather than using default renderer and parser
classes in your settings. If you need a customized version of the
obtain_auth_token view, you can do so by overriding the
ObtainAuthToken view class, and using that in your url conf instead
As for now, my attempt looks like this:
urlpatterns = [
url(r'^api-token-auth/', my_customized_view),
]
class Foo(ObtainAuthToken):
def post(self):
# here goes my customized code
my_customized_view = Foo.as_view()
The odds are that my code looks quite silly, and I am just lost trying to google it. I have little experience in Djagno, so please help me with this !
I have just been going through the same journey for comprehension as I wished to return the user and also allow email or username login. The documentation is not entirely clear, but as described for auth token, you can do the same for JWT.
obtain_auth_token is to ObtainAuthToken, as obtain_jwt_token is to ObtainJSONWebToken.
This is my overwritten login method:
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.views import ObtainJSONWebToken
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class LoginView(ObtainJSONWebToken):
def post(self, request, *args, **kwargs):
# by default attempts username / passsword combination
response = super(LoginView, self).post(request, *args, **kwargs)
# token = response.data['token'] # don't use this to prevent errors
# below will return null, but not an error, if not found :)
res = response.data
token = res.get('token')
# token ok, get user
if token:
user = jwt_decode_handler(token) # aleady json - don't serialize
else: # if none, try auth by email
req = request.data # try and find email in request
email = req.get('email')
password = req.get('password')
username = req.get('username')
if email is None or password is None:
return Response({'success': False,
'message': 'Missing or incorrect credentials',
'data': req},
status=status.HTTP_400_BAD_REQUEST)
# email exists in request, try to find user
try:
user = User.objects.get(email=email)
except:
return Response({'success': False,
'message': 'User not found',
'data': req},
status=status.HTTP_404_NOT_FOUND)
if not user.check_password(password):
return Response({'success': False,
'message': 'Incorrect password',
'data': req},
status=status.HTTP_403_FORBIDDEN)
# make token from user found by email
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user = UserSerializer(user).data
return Response({'success': True,
'message': 'Successfully logged in',
'token': token,
'user': user},
status=status.HTTP_200_OK)
You can change the default to check by email only if you please by customising Django's auth model, but I was happy to have both options.
I started creating an api boilerplate. There is a requirements.txt file and a config.example.py file for anyone who wants to pull it down to view the rest.
https://github.com/garyburgmann/django-api-boilerplate
In views.py, file add the following code and customize as you want.
def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': UserSerializer(user, context={'request': request}).data
}
Default is {'token': token}
Also In your settings.py file add
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER':
'api.user.views.jwt_response_payload_handler',
}
( 'api.user.views.jwt_response_payload_handler',
) is the path to your custom jwt_response_payload_handler
For more help, you can view here

How to sign requests with custom access token for 2 legged oAuth

I using oauth-ruby gem for a while, and I already implemented 2 types of auth:
default one
and custom, which uses OTP sent via sms
Both of them works perfectly now
But now i'm trying to implement new(3) 2-legged oauth. And I ran in to issues which I actually can't understand.
All my signed requests using access token from (3) are failing because of invalid token. For (1-2) it works without any issues.
Signing requests is implemented via RestClient.before_execution_proc:
RestClient.add_before_execution_proc do |req, params|
access_token.sign!(req)
end
I suppose the problem comes from access_token = OAuth::AccessToken as there actual difference between other 2.
Any suggestions or advices will be very helpful
1.
def default_oauth(login, pass, device: Device.new)
#cookies = login_req(login, pass).cookies
headers = common_headers.merge("Cookie" => #cookies)
#Get request token
request_token = consumer.get_request_token
# Authorize request key
authorize = RestClient.post(base_url + '/oauth/authorize',
{ requestToken: request_token.token, authorize: 'Authorize'},
headers) {|response, request, result| response }
auth_url_resp = RestClient.get(authorize.headers[:location], headers: headers) {|response, request, result| response }
# Get Access key
access_token = request_token.get_access_token
end
2.
def custom_oauth(phone, pin, otp: nil, device: Device.new)
otp = phone.to_s[-5..-1] if otp.nil?
resp = RestClient.post("#{base_url}/rest/smartphone/otp/sms-sender/#{phone}", '', common_headers) {|response, request, result| response }
request_token = consumer.get_request_token
payload = {
device: device.to_h,
otp: otp,
password: pin.to_s,
requestToken: request_token.token
}
headers = json_headers.merge('Cookie' => otp)
authorize = RestClient.post(base_url + '/oauth/otp-authorization',
payload.to_json, headers) {|response, request, result| response }
#access_token = request_token.get_access_token
end
3.
def new_oauth(login, pass, entry, device: Device.new)
tkn = consumer.get_request_token.token
payload = {
username: login,
password: pass.to_s,
requestToken: tkn,
entryPoint: entry,
device: device.to_h
}
headers =json_headers(device.id)
resp = RestClient.post("#{base_url}/oauth/login-authorization", payload.to_json, headers) {|response, request, result| response}
hsh ={oauth_token: resp.headers[:accesstoken], oauth_token_secret: resp.headers[:tokensecret] }
access_token = OAuth::AccessToken.from_hash(consumer, hsh)
end
Consumer:
def consumer
#consumer ||= build_consumer
end
def build_consumer
key = 'key_string'
secret ='secret_string'
OAuth::Consumer.new(key, secret, :site => base_url)
end
Issue was related to server (Spring) encoding stuff. oauth-ruby gem is escaping token secret (Combined secret or Encryption key) which is used for signature creation. Spring by default doing the same on server side.
Unescaping access_token.secret fixed this issue:
access_token.secret = OAuth::Helper.unescape(access_token.secret)

Resources