Add custom field to Django Rest Framework generation token - oauth-2.0

With Django Rest Framework i try to add a custom field (clan) when i want to generate token, it should be a required field and an existing clan.
Like :
curl -X POST -d "grant_type=password&username=username&password=password&client_id=client_id&client_secret=client_secret&clan=ABCDEF" http://localhost:8000/o/token/
My User model :
class User(AbstractUser):
"""
User model.
"""
clans = models.ManyToManyField(Clan, related_name='Users')
My rest framework settings:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'apps.core.api.pagination.StandardPagination',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAdminUser',
),
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'EXCEPTION_HANDLER': 'apps.core.exceptions.api.custom_exception_handler',
}
Then i want a classic response:
{
"access_token": "azerty123456789",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "read write groups",
"refresh_token": "azerty123456789"
}

Finally i found the solution :
You need to override the class TokenView from oauth2_provider.views.base then override the post method.
class MyCustomToken(TokenView):
"""
....
"""
#method_decorator(sensitive_post_parameters("password"))
def post(self, request, *args, **kwargs):
"""
...
"""
# Check if the clan parameter is given in parameter.
clan_parameter = request.POST.get('clan', False)
if not clan_parameter:
return JsonResponse(
{'clan': 'Parameter is required.'},
status = 401
)
url, headers, body, status = self.create_token_response(request)
if status == 200:
access_token = json.loads(body).get("access_token")
if access_token is not None:
token = get_access_token_model().objects.get(
token=access_token)
app_authorized.send(
sender=self, request=request,
token=token)
response = HttpResponse(content=body, status=status)
for k, v in headers.items():
response[k] = v
return response

Related

Sign In with Apple, decoded Apple response

I've implemented 'Sign In with Apple' from this source (https://gist.github.com/aamishbaloch/2f0e5d94055e1c29c0585d2f79a8634e?permalink_comment_id=3328115) taking into account the comments of NipunShaji and aj3sh. But it doesn't works because Apple sends incomplete data: I recieve
decoded = {'iss': 'https://appleid.apple.com', 'aud': '...', 'exp': 1664463442, 'iat': 1664377042, 'sub': '.....', 'at_hash': '....', 'auth_time': 1664377030, 'nonce_supported': True}
without email data).
According to the Apple's documentation typical response contains email: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple.
What I've missed?
Additional code:
view.py file:
class AppleSocialAuthView(GenericAPIView):
serializer_class = AppleSocialAuthSerializer
permission_classes = [AllowAny]
def post(self, request):
"""
POST with "auth_token"
Send an access token as from facebook to get user information
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
data = (serializer.validated_data['auth_token'])
return Response(data, status=status.HTTP_200_OK)
serializer.py file:
class Apple(BaseOAuth2):
"""apple authentication backend"""
name = 'apple'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
#handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
"""
Finish the auth process once the access_token was retrieved
Get the email from ID token received from apple
"""
response_data = {}
client_id, client_secret = self.get_key_and_secret()
headers = {'content-type': "application/x-www-form-urlencoded"}
data = {
'client_id': client_id,
'client_secret': client_secret,
'code': access_token,
'grant_type': 'authorization_code',
'redirect_uri': settings.SOCIAL_AUTH_APPLE_REDIRECT_URL
}
res = requests.post(Apple.ACCESS_TOKEN_URL, data=data, headers=headers)
response_dict = res.json()
id_token = response_dict.get('id_token', None)
if id_token:
decoded = jwt.decode(id_token, '', options={"verify_signature": False}, verify=False)
print(decoded)
response_data.update({'email': decoded['email']}) if 'email' in decoded else None
response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None
response = kwargs.get('response') or {}
response.update(response_data)
response.update({'access_token': access_token}) if 'access_token' not in response else None
kwargs.update({'response': response, 'backend': self})
return self.strategy.authenticate(*args, **kwargs)
def get_user_details(self, response):
email = response.get('email', None)
details = {
'email': email,
}
return details
def get_key_and_secret(self):
headers = {
'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID,
'alg': 'ES256',
}
payload = {
'iss': settings.SOCIAL_AUTH_APPLE_TEAM_ID,
'iat': int(time.time()),
'exp': int(time.time()) + 15552000,
'aud': 'https://appleid.apple.com',
'sub': settings.SOCIAL_AUTH_APPLE_CLIENT_ID,
}
client_secret = jwt.encode(
payload,
settings.SOCIAL_AUTH_APPLE_CLIENT_SECRET,
# algorithm='ES256',
headers=headers
)
return settings.SOCIAL_AUTH_APPLE_CLIENT_ID, client_secret
class AppleSocialAuthSerializer(serializers.Serializer):
auth_token = serializers.CharField()
def validate_auth_token(self, auth_token):
user_data = Apple()
user_data = user_data.do_auth(auth_token)
try:
email = user_data['email']
name = user_data['name']
provider = 'apple'
return register_social_user(
provider=provider, email=email, name=name)
except Exception as identifier:
raise serializers.ValidationError(
'The token is invalid or expired. Please login again.'
)
When I test this proces on my Mac (logging into web app), the end result is that I can see on my Mac, preferences -> Apple ID, that I'm using SSO for this application.
So it looks like Apple validated this Web App.
If they do send email, only first time the user is logging in to Web App, how Web App should know next time what user to log in?
There is no single parameter that would identify the user in decoded response (like some ID, which would also appear in their first response?
Best Regards, Marek

Apache Superset and Auth0 returns "The browser (or proxy) sent a request that this server could not understand."

I'm trying to set up Superset with Auth0. I've found somewhat similar issues here and here.
I've set up the following configuration based on the first link above and trying to follow the Superset and Flask-AppBuilder docs:
from flask_appbuilder.security.manager import (
AUTH_OAUTH,
)
from superset.security import SupersetSecurityManager
import json
import logging
import string
import random
nonce = ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k = 30))
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Admin"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [
{ 'name':'auth0',
'token_key':'access_token',
'icon':'fa-at',
'remote_app': {
'api_base_url': AUTH0_URL,
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'server_metadata_url': AUTH0_URL + '/.well-known/openid-configuration',
'client_kwargs': {
'scope': 'openid profile email'
},
'response_type': 'code token',
'nonce': nonce,
}
}
]
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
logger.debug('oauth2 provider: {0}'.format(provider))
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get(AUTH0_URL + '/userinfo')
logger.debug('response: {0}'.format(res))
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.json())
return
# user_info = self.appbuilder.sm.oauth_remotes[provider].parse_id_token(res)
# logger.debug('user_info: {0}'.format(user_info))
me = res.json()
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
}
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
The full error log message is:
2022-03-18 18:53:56,854:ERROR:flask_appbuilder.security.views:Error authorizing OAuth access token: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
NOTES:
I can see an access_token parameter in the redirect url, so it seems to be working with Auth0 correctly.
I don't see any of the debug lines in the CustomSsoSecurityManager being written, so my guess is that I have not correctly set that up (or my logging is not correctly configured).
I've tried using both Regular Web Application and Single Page Application application types in Auth0, and both fail in the same way.
I would appreciate any help in understanding what I might be missing or what else I need to do to configure Auth0 to work with Superset.
I was able to make it work using the JSON Web Key Set endpoint provided by Auth0, look at this example and adapt it accordingly:
from jose import jwt
from requests import request
from superset.security import SupersetSecurityManager
class CustomSecurityManager(SupersetSecurityManager):
def request(self, url, method="GET", *args, **kwargs):
kwargs.setdefault("headers", {})
response = request(method, url, *args, **kwargs)
response.raise_for_status()
return response
def get_jwks(self, url, *args, **kwargs):
return self.request(url, *args, **kwargs).json()
def get_oauth_user_info(self, provider, response=None):
if provider == "auth0":
id_token = response["id_token"]
metadata = self.appbuilder.sm.oauth_remotes[provider].server_metadata
jwks = self.get_jwks(metadata["jwks_uri"])
audience = self.appbuilder.sm.oauth_remotes[provider].client_id
payload = jwt.decode(
id_token,
jwks,
algorithms=["RS256"],
audience=audience,
issuer=metadata["issuer"],
)
first_name, last_name = payload["name"].split(" ", 1)
return {
"email": payload["email"],
"username": payload["email"],
"first_name": first_name,
"last_name": last_name,
}
return super().get_oauth_user_info(provider, response)

Apache Superset and Auth0 returns "unexpected keyword argument 'scope'"

Some time ago I've successfully integrated Superset authentication with Oauth using AWS Cognito.
Now I'm trying to do the same with Auth0, reusing the previous configuration and changing the endpoints according to Auth0 documentation.
Unfortunately, the login fails and Superset's log returns the following message:
2021-10-20 10:30:48,886:ERROR:flask_appbuilder.security.views:Error on OAuth authorize: request() got an unexpected keyword argument 'scope'
This is the Oauth configuration in superset_config.py:
from superset.security import SupersetSecurityManager
import json
import logging
logger = logging.getLogger(__name__)
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.data)
return
me = json.loads(res._content)
logger.warning(" user_data: %s", me)
prefix = 'Superset'
logging.warning("user_data: {0}".format(me))
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
'first_name': me['given_name'],
'last_name': me['family_name'],
}
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [{
'name':'auth0',
'token_key': 'access_token',
'icon':'fa-at',
'url': AUTH0_URL,
'remote_app': {
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'request_token_params': {
'scope': 'email openid profile'
},
'response_type': 'token_id',
'base_url': AUTH0_URL,
'access_token_url': os.path.join(AUTH0_URL, 'oauth/token'),
'authorize_url': os.path.join(AUTH0_URL, 'authorize'),
'access_token_method':'POST',
'request_token_url': os.path.join(AUTH0_URL, 'oauth/token'),
'api_base_url': AUTH0_URL,
}
}
]
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
I have already tried different values for the response_type (code, token, token_id).
Also tried to leave request_token_url empty and in that case the error changes because the user data appear to be an empty dictionary:
2021-10-13 15:52:10,358:WARNING:superset_config: user_data: {}
2021-10-13 15:52:10,358:WARNING:root:user_data: {}
2021-10-13 15:52:10,358:ERROR:flask_appbuilder.security.views:Error returning OAuth user info: 'email'
So I assume the token is actually returned and I cannot understand why Flask is complaining about the attribute "scope".
Tried this too, since it looked like very similar to my problem, but none of those configurations work for me.
Hope you have two files as custom_sso_security_manager.py and superset_config.py
Can you remove below two line from return and try(custom_sso_security_manager.py).
'first_name': me['given_name'],
'last_name': me['family_name'],
This is for future reference, although I accepted Kamal's answer.
It turned out that the right parameter to set the request token scopes was client_kwargs instead of request_token_params.
This is a working configuration to authenticate Superset against Auth0:
## Enable OAuth authentication
from flask_appbuilder.security.manager import (
AUTH_OAUTH,
)
from superset.security import SupersetSecurityManager
import json
import logging
import string
import random
nonce = ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k = 30))
logger = logging.getLogger(__name__)
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.json())
return
me = res.json()
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
}
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Public"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [
{ 'name':'auth0',
'token_key':'access_token',
'icon':'fa-at',
'remote_app': {
'api_base_url': AUTH0_URL,
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'server_metadata_url': os.path.join(AUTH0_URL, '.well-known/openid-configuration'),
'client_kwargs': {
'scope': 'openid profile email'
},
'response_type': 'code token',
'nonce': nonce,
}
}
]
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
As per Flask Documentation,
try to use client_kwargs instead of request_token_params key.
Sample:
{
'name':'google',
'icon':'fa-google',
'token_key':'access_token',
'remote_app': {
'client_id':'GOOGLE_KEY',
'client_secret':'GOOGLE_SECRET',
'api_base_url':'https://www.googleapis.com/oauth2/v2/',
'client_kwargs':{
'scope': 'email profile'
},
'request_token_url':None,
'access_token_url':'https://accounts.google.com/o/oauth2/token',
'authorize_url':'https://accounts.google.com/o/oauth2/auth'
}
},

How to return access token on login with oauth2 in drf?

I want to return the user access token for oauth2 as soon as the user logs in with a login api.
Till now I have created a login and register api and I am able to genereate access token via /o/token but I want it as a return value.
Here is my views.py :-
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (AllowAny,)
serializer_class = UserSerializer
queryset = User.objects.all()
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = authenticate(request, username=username, password=password)
if user is not None:
# login saves the user’s ID in the session,
# using Django’s session framework.
login(request, user)
return redirect('list-user')
return Response(status=status.HTTP_401_UNAUTHORIZED)
class RegisterUserView(generics.CreateAPIView):
"""
POST auth/register/
"""
permission_classes = (AllowAny,)
serializer_class = UserRegistrationSerializer
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
email = request.data.get("email", "")
if not username and not password and not email:
return Response(
data={
"message": "username, password and email is required to register a user"
},
status=status.HTTP_400_BAD_REQUEST
)
new_user = User.objects.create_user(
username=username, password=password, email=email
)
return Response(status=status.HTTP_201_CREATED)
and here is my serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
class UserRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email','password']
urls.py
path('admin/', admin.site.urls),
path('', include('users.urls')),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
urls.py for users app
urlpatterns = [
path('users/', views.UserListView.as_view(), name='list-user'),
path('auth/login/', views.LoginView.as_view(), name="auth-login"),
path('auth/register/', views.RegisterUserView.as_view(), name="auth-register")
]
How can I implement it?
You will have to get token from oauth2_provider view for auth token generation and and then you can modify response according to your requirement.
from oauth2_provider.views.base import TokenView
class CustomAuthView(generics.CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request, *args, **kwargs):
oauth_response = TokenView.as_view(request, *args, **kwargs)
if oauth_response.status == 200:
data = oauth_response.data
# update data according to your requirement
return response.Response(data)
else:
return oauth_response
If you want to just change url for auth view, then you can do it by adding a new url which will point towards TokenView like this
from oauth2_provider.views.base import TokenView
path('auth/login/', TokenView.as_view(), name="auth-login"),
You will have to provide following parameters to your api
grant_type
username
password
client_id
client_secret

Django OAuth custom Authentication class

Need to override the current authentication view api/v1/o/token to add my custom error messages based on username and password.
1.
{
"status = ok // need to add this
"access_token": "xxxx",
"token_type": "Bearer",
"expires_in": 60,
"refresh_token": "xxxxaaaxxxx",
"scope": "read write"
}
2.
status = 'not_active'
detail= 'user not activated'
3.
status = 'error'
detail= 'Incorrect username or password'
I want to disable the application create on my production hosting.
How can I do that.?
This is how you create a custom authentication class with Django Rest Framework. Subclass BaseAuthentication and override the .authenticate(self, request) method.
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class CustomAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
"""
Consider the method validate_access_token() takes an access token,
verify it and return the User.username if the token is valid else None
"""
username = validate_access_token(request.META.get('X_ACCESS_TOKEN'))
if not username:
return None #return None if User is not authenticated.
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
then change DEFAULT_AUTHENTICATION_CLASSES in settings to point to the custom authentication class
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'api.core.auth.CustomAuthentication',
),
}

Resources