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

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)

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 "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

403 Response From Adobe Experience Manager OAuth 2 Token Endpoint

I am using Postman to test OAuth 2 from a vanilla AEM install.
Postman can successfully obtain the authorization code from /oauth/authorize after I grant access:
But when it tries to use the code to obtain a token from /oauth/token it receives the following response:
HTTP ERROR: 403 Problem accessing /oauth/token. Reason: Forbidden
Powered by Jetty://
Looking in Fiddler it is doing a POST to /oauth/token with the following Name/Values in the body:
client_id: Client ID from /libs/granite/oauth/content/client.html
client_secret:
Client Secret from /libs/granite/oauth/content/client.html
redirect_uri: https://www.getpostman.com/oauth2/callback
grant_type: authorization_code
code: Code returned from previous request to oauth/authorize
Am I missing something?
Would help if you can list some code snippets on how you are building the url and fetching the token.
Here's an example of how we've implemented very similar to what you are trying to do, maybe it'll help.
Define a service like below (snippet) and define the values (host, url, etc) in OSGI (or you can also hard code them for testing purposes)
#Service(value = OauthAuthentication.class)
#Component(immediate = true, label = "My Oauth Authentication", description = "My Oauth Authentication", policy = ConfigurationPolicy.REQUIRE, metatype = true)
#Properties({
#Property(name = Constants.SERVICE_VENDOR, value = "ABC"),
#Property(name = "service.oauth.host", value = "", label = "Oauth Host", description = "Oauth Athentication Server"),
#Property(name = "service.oauth.url", value = "/service/oauth/token", label = "Oauth URL", description = "Oauth Authentication URL relative to the host"),
#Property(name = "service.oauth.clientid", value = "", label = "Oauth Client ID", description = "Oauth client ID to use in the authentication procedure"),
#Property(name = "service.oauth.clientsecret", value = "", label = "Oauth Client Secret", description = "Oauth client secret to use in the authentication procedure"),
#Property(name = "service.oauth.granttype", value = "", label = "Oauth Grant Type", description = "Oauth grant type") })
public class OauthAuthentication {
...
#Activate
private void activate(ComponentContext context) {
Dictionary<String, Object> properties = context.getProperties();
host = OsgiUtil.toString(properties, PROPERTY_SERVICE_OAUTH_HOST,new String());
// Similarly get all values
url =
clientID =
clientSecret =
grantType =
authType = "Basic" + " "+ Base64.encode(new String(clientID + ":" + clientSecret));
}
public static void getAuthorizationToken(
try {
UserManager userManager = resourceResolver.adaptTo(UserManager.class);
Session session = resourceResolver.adaptTo(Session.class);
// Getting the current user
Authorizable auth = userManager.getAuthorizable(session.getUserID());
user = auth.getID();
password = ...
...
...
String serviceURL = (host.startsWith("http") ? "": protocol + "://") + host + url;
httpclient = HttpClients.custom().build();
HttpPost httppost = new HttpPost(serviceURL);
// set params
ArrayList<BasicNameValuePair> formparams = new ArrayList<BasicNameValuePair>();
formparams.add(new BasicNameValuePair("username", user));
formparams.add(new BasicNameValuePair("password", password));
formparams.add(new BasicNameValuePair("client_id", clientID));
formparams.add(new BasicNameValuePair("client_secret",clientSecret));
formparams.add(new BasicNameValuePair("grant_type",grantType));
UrlEncodedFormEntity postEntity = new UrlEncodedFormEntity(formparams, "UTF-8");
httppost.setEntity(postEntity);
// set header
httppost.addHeader("Authorization", authType);
response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
if (response.getStatusLine().getStatusCode() == 200) {
if (entity != null) {
object = new JSONObject(EntityUtils.toString(entity));
}
if (object != null) {
accessToken = object.getString("access_token");
////
}
}
}
I found the answer myself and thought I'd share the process I went through as well as the answer because it might help other people new to AEM.
How to find the cause of the error:
Go to CRXDE Lite.
Select console.
Then deselect the stop button to allow new console logs to appear (this is very counter-intuitive to me).
From here I was able to see the cause of the issue:
org.apache.sling.security.impl.ReferrerFilter Rejected empty referrer header for POST request to /oauth/token
Because postman does not place a referrer in the request header I had to tell Apache Sling to allow empty request headers.
To do this:
Go to /system/console/configMgr
Open the Apache Sling Referrer Filter Config
Select the Allow Empty check box
Good way to allow this to list the allowed hosts, otherwise this is against best practices for AEM security checklist.
Its fine for development environment not for production.

oauth facebook with grails accessing token

Its a grails project,
Facebook authentication is successful via oauth,
Now when it comes back to my controller, I want to get emailID of the logged in user,
Searched a lot, but did not find proper documentation,
I am using scribe and have following code in Config.groory
import org.scribe.builder.api.FacebookApi
oauth {
providers {
facebook {
api = FacebookApi
key = 'xxxx'
secret = 'yyyy'
callback = "http://my-domain-name-here:8080/TestOAuth2/dashBoard/facebooklogin"
successUri = "http://my-domain-name-here:8080/TestOAuth2/dashBoard/success"
}
}
}
Any help much appreciated.
Thanks.
Try this..,.
Config:
import org.scribe.builder.api.FacebookApi
...
oauth {
providers {
facebook {
api = FacebookApi
key = 'XXX'
secret = 'YYY'
scope = 'email,read_stream,publish_actions,user_birthday,publish_stream'
callback = "http://localhost:8080/appName/oauth/facebook/callback" //callback to oauth controller of oauth plugin
successUri = "http://localhost:8080/appName/myController/facebookSuccess"
failureUri = "http://localhost:8080/appName/myController/facebookFailure"
}
}
}
MyController:
def facebookSuccess() {
Token facebookAccessToken = (Token) session[oauthService.findSessionKeyForAccessToken('facebook')]
def facebookResource = oauthService.getFacebookResource(facebookAccessToken, "https://graph.facebook.com/me")
def facebookResponse = JSON.parse(facebookResource?.getBody())
log.info "Email = ${facebookResponse.email}"
...
}
You can get working example from my git repo. Grails Oauth Plugin Demo.
Email is not part of a Facebook public_profile. The only way to get the users e-mail address is to request extended permissions on the email field. You can do this by adding a scope to the oauth provider.
config.groovy
oauth {
providers {
facebook {
api = org.scribe.builder.api.FacebookApi
scope = 'email'
...
...
}
}
}
As an example of how to return email and various public_profile fields please see below.
Take Note of: getFacebookResource params e.g. https://graph.facebook.com/me?fields=id,name,verified,age_range,email"
import grails.converters.JSON
import org.scribe.model.Token
import grails.plugin.springsecurity.oauth.OAuthToken
class SpringSecurityOAuthController {
def oauthService
def onSuccess = {
// Validate the 'provider' URL. Any errors here are either misconfiguration
// or web crawlers (or malicious users).
if (!params.provider) {
renderError 400, "The Spring Security OAuth callback URL must include the 'provider' URL parameter."
return
}
def sessionKey = oauthService.findSessionKeyForAccessToken(params.provider)
if (!session[sessionKey]) {
renderError 500, "No OAuth token in the session for provider '${params.provider}'!"
return
}
// Create the relevant authentication token and attempt to log in.
OAuthToken oAuthToken = createAuthToken(params.provider, session[sessionKey])
Token facebookAccessToken = (Token) session[oauthService.findSessionKeyForAccessToken('facebook')]
def facebookResource = oauthService.getFacebookResource(facebookAccessToken , "https://graph.facebook.com/me?fields=id,name,verified,age_range,email")
def facebookResponse = JSON.parse(facebookResource?.getBody())
println facebookResponse
...
...
}
}
public_profile (Default)
A person's public profile refers to the following properties on the user object by default:
id cover
name
first_name
last_name
age_range
link
gender
locale
picture
timezone
updated_time
verified

Resources