Using GitHub OAuth for Superset - flask

I am running superset from a container on a server and I am trying to use GitHub OAuth for user signup/login.
Unfortunately I got it working to the point where I can see the "SIGN IN WITH GITHUB" button. However when I click it, the website populates a label that reads "The request to sign in was denied.".
Looking at the docker logs of the app, I've tracked down the issue to a Flask error:
ERROR:flask_appbuilder.security.views:Error authorizing OAuth access token: redirect_uri_mismatch: The redirect_uri MUST match the registered callback URL for this application.
I'm not sure what I'm doing wrong. I believe it may be because I configured the wrong Authorization Callback URL when creating the OAuth app on GitHub. Does superset have a default Authorization Callback URL that I'm supposed to use? I have mine currently set as https://my-domain.com/oauth-authorized/github.
My other theory is that the custom_sso_security_manager.py is not configured properly due to me using the default values. If that's the case, could anyone point me in the right direction?
Here is my superset config file:
from flask_appbuilder.security.manager import AUTH_OAUTH
from custom_sso_security_manager import CustomSsoSecurityManager
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [
{ 'name':'github',
'token_key':'access_token',
'icon':'fa-github',
'remote_app': {
'client_id':'"#############################',
'client_secret':'##############################',
'client_kwargs':{
'scope':'read'
},
'access_token_method':'POST',
'access_token_params':{
'client_id':'#############################'
},
'access_token_headers':{
'Authorization': 'Basic Base64EncodedClientIdAndSecret'
},
'api_base_url':'https://api.github.com/user',
'access_token_url':'https://github.com/login/oauth/access_token',
'authorize_url':'https://github.com/login/oauth/authorize',
'redirect_uri':'https://my-domain.com/oauth-authorized/github'
}
}
]
# Will allow user self registration, allowing to create Flask users from Authorized User
AUTH_USER_REGISTRATION = True
# The default user self registration role
AUTH_USER_REGISTRATION_ROLE = ["sql_lab","workshop"]
And here is my custom_sso_security_manager.py:
import logging
from superset.security import SupersetSecurityManager
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
logging.debug("Oauth2 provider: {0}.".format(provider))
if provider == 'github':
# As example, this line request a GET to base_url + '/' + userDetails with Bearer Authentication,
# and expects that authorization server checks the token, and response with user details
me = self.appbuilder.sm.oauth_remotes[provider].get('userDetails').data
logging.debug("user_data: {0}".format(me))
return { 'name' : me['name'], 'email' : me['email'], 'id' : me['user_name'], 'username' : me['user_name'], 'first_name':me['nickname'], 'last_name':me['nickname']}

Related

Google OAuth RefreshError even with Refresh Token present in the Django app

My app running on Django with Django allauth allows signup/login only via Google OAuth. Therefore when a user is registered, his social oauth details are saved to All-auth Social Accounts model and the token data saved in Social Application Tokens model.
We want the users to be logged-in as long as possible, without granting/revoking access regularly.
Question 1: The default expiry is set as 1 hour from the registration time. How can I extend this programmatically and for how long (can it be never-expire?)
Currently, a user logged in at 5:00 and I get the following data from credentials.to_json():
{
"token": "ya29.A0ARrdaM8YXPM35RXPv7UK-pXLZvWG49T-MgCZ5wMse2ADMXOZJOWFJKMaq1PkobADLptM5YX5mnrliS2yCCESqCk0NTaZJkfe6inK94j6WQMFZWIT_xRyBTOX4W3dUEiuLhHFpQcD5vS-x_Y22pUzxwgI23pp",
"refresh_token": "1//0gYC8oucHhTBVCgYIARAAGBASNwF-L9IrCG7c5IJCBMVznUrytGEFsJbsObAFvmNBoQbHHGA1KESyBWgmudEVogbes8ski87q_5g",
"client_id": "blablabla.apps.googleusercontent.com",
"client_secret": "xyzsecret"}
No other data is returned.
At 6:05, the credentials.to_json() is exactly the SAME AS ABOVE.
But to fetch any Google/Youtube API data, I get the following error in server logs:
google.auth.exceptions.RefreshError: The credentials do not contain the necessary fields need to refresh the access token.
Question 02: When there's a Refresh Token available already, why the error?
As per the docs, it refreshes the token automatically few minutes before the expiry. What am I missing?
I already had "access_type": "offline" in the providers settings. I also tried adding "prompt": "consent", but no effect.
Django Settings:
INSTALLED_APPS = [
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
...
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
SOCIALACCOUNT_PROVIDERS = {
"google": {
"SCOPE": [
"profile",
"email",
"https://www.googleapis.com/auth/youtube",
"https://www.googleapis.com/auth/youtube.readonly",
"https://www.googleapis.com/auth/youtube.upload",
"https://www.googleapis.com/auth/youtube.force-ssl",
],
"AUTH_PARAMS": {
"access_type": "offline",
"prompt": "consent",
},
},
}
And the Django Views snippet related to OAuth:
import googleapiclient.discovery
import googleapiclient.errors
from allauth.socialaccount.models import SocialAccount, SocialApp
from google.oauth2.credentials import Credentials
def get_credentials_google(user):
app_google = SocialApp.objects.get(provider="google")
account = SocialAccount.objects.get(user=user)
user_tokens = account.socialtoken_set.first()
creds = Credentials(
token=user_tokens.token,
refresh_token=user_tokens.token_secret,
client_id=app_google.client_id,
client_secret=app_google.secret,
)
return creds
def get_youtube_account(user):
api_service_name = "youtube"
api_version = "v3"
credentials = get_credentials_google(user)
youtube = googleapiclient.discovery.build(
api_service_name, api_version, credentials=credentials
)
return youtube
def get_youtube_videos(request):
youtube = get_youtube_account(request.user)
request = youtube.liveBroadcasts().list(
part="id, snippet, contentDetails, status",
broadcastStatus="completed",
broadcastType="all"
)
response = request.execute()
return response
Note: There's no front-end framework I'm using django with django template UI.
How can I extend this programmatically and for how long (can it be never-expire?)
You cant access tokens expire after one hour this is standard. You can use the refresh token to request a new access token as you need
When there's a Refresh Token available already, why the error? As per the docs, it refreshes the token automatically few minutes before the expiry. What am I missing?
Refresh tokens are used to request a new access token approximately five minutes before its due to expire.
google.auth.exceptions.RefreshError: The credentials do not contain the necessary fields need to refresh the access token.
Sounds like your not setting the client object properly. Something here is not set right. Also remember if your app is still in testing your refresh token will expire after seven days and you will need to reauthorize your test users.
creds = Credentials(
token=user_tokens.token,
refresh_token=user_tokens.token_secret,
client_id=app_google.client_id,
client_secret=app_google.secret,
)

Django-Rest-Framework system to check custom HTTP header (application - token)

I use Django and Django- rest-framework.
I have to check a custom http header for a lot of my views.
For each view I need to:
Check if the http custom header is there (X-APP-TOKEN);
Check if this token is correct;
Serve the request or return an HTTP error (for example 403);
Is there some approach that I can follow?
For example something like permissions_class for rest-framework view.
I tried to implement a custom permission like this:
class IsAuthorizedApplication(BasePermission):
def has_permission(self, request, view):
app_id = request.META.get(app_settings.APPS_HEADER_AUTHORIZATION_APP_ID)
secret_token = request.META.get(app_settings.APPS_HEADER_AUTHORIZATION_APP_TOKEN)
if app_id and secret_token:
try:
selected_app = Application.objects.get(app_uuid=app_id, status=ApplicationStatusType.ACTIVE)
// Check secret token
return True
except Application.DoesNotExist:
return False
return False
But I think that this approach is based to the authentication system of djnago-rest-framework. Infact in case of 'false return' I receive:
401 - {"detail":"Authentication credentials were not provided."}
Is there some different approach to check custom http headers like the permission-class or have I to write a base View to check the application-token before to serve the request?
you can use this
https://pypi.org/project/djangorestframework-api-key/
Install the latest version with pip:
pip install djangorestframework-api-key
# settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
"rest_framework_api_key",
]
be sure that "rest_framework_api_key", cames after "rest_framework",
Run the included migrations:
python manage.py migrate
then from admin create new key
now
HasAPIKey permission class protects a view behind API key authorization.
You can set the permission globally:
# settings.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework_api_key.permissions.HasAPIKey",
]
}
or on a per-view basis:
# views.py
from rest_framework.views import APIView
from rest_framework_api_key.permissions import HasAPIKey
class UserListView(APIView):
permission_classes = [HasAPIKey]
# ...
Authorization header
By default, clients must pass their API key via the Authorization header. It must be formatted as follows:
Authorization: Api-Key ********
where ******** refers to the generated API key.
or you can do this
For example, if you set:
# settings.py
API_KEY_CUSTOM_HEADER = "X-APP-TOKEN"
then clients must make authorized requests using:
X-APP-TOKEN: ********
it seems like this response is return by the django auth brcause you have pass auth token in header, for the permission above code looks fine. It should return the 403.
please check the the default setting of the djnago restframework
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
If not specified, this setting defaults to allowing unrestricted access:
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
in order to implement custom authentication you need to inherit from "BaseAuthentication" and override the authenticate method it must return None for non-authenticated users or (user, auth) for authenticated users refer to docs for more info
https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication

How do I create an Access Token from Service Account Credentials using REST API?

I have created a Service Account in Google Cloud Platform and downloaded the Private Key in JSON format. I am trying to create a Compute resource via REST API. For authentication purpose, I need an AccessToken which needs to be set as a Header of create compute resource REST API. Is there a REST API to get the Access Token from the Private Key (Without using SDK or Google Clients)?
The following example shows you several important steps to call Google Cloud APIs without using an SDK in Python. Similar code works in just about any language (c#, java, php, nodejs).
Change the source code with the filename of your service account Json file, your Google Zone and your Project ID.
This example will list the instances in one zone for the specified project. From this example you will know the framework to call an API to create GCE instances.
This code will show you how to:
How to load service account credentials from a Json file.
How to extract the Private Key used to sign requests.
How to create a JWT (Json Web Token) for Google Oauth 2.0.
How to set the Google Scopes (permissions).
How to sign a JWT to create a Signed-JWT (JWS).
How to exchange the Signed-JWT for a Google OAuth 2.0 Access Token.
How to set the expiration time. This program defaults to 3600 seconds (1 Hour).
How to call a Google API and set the Authorization Header.
How to process the returned Json results and display the name of each instance.
Example program in Python 3.x:
'''
This program lists lists the Google Compute Engine Instances in one zone
'''
# Author: John Hanley
# https://www.jhanley.com
import time
import json
import jwt
import requests
import httplib2
# Project ID for this request.
project = 'development-123456'
# The name of the zone for this request.
zone = 'us-west1-a'
# Service Account Credentials, Json format
json_filename = 'service-account.json'
# Permissions to request for Access Token
scopes = "https://www.googleapis.com/auth/cloud-platform"
# Set how long this token will be valid in seconds
expires_in = 3600 # Expires in 1 hour
def load_json_credentials(filename):
''' Load the Google Service Account Credentials from Json file '''
with open(filename, 'r') as f:
data = f.read()
return json.loads(data)
def load_private_key(json_cred):
''' Return the private key from the json credentials '''
return json_cred['private_key']
def create_signed_jwt(pkey, pkey_id, email, scope):
''' Create a Signed JWT from a service account Json credentials file
This Signed JWT will later be exchanged for an Access Token '''
# Google Endpoint for creating OAuth 2.0 Access Tokens from Signed-JWT
auth_url = "https://www.googleapis.com/oauth2/v4/token"
issued = int(time.time())
expires = issued + expires_in # expires_in is in seconds
# Note: this token expires and cannot be refreshed. The token must be recreated
# JWT Headers
additional_headers = {
'kid': pkey_id,
"alg": "RS256",
"typ": "JWT" # Google uses SHA256withRSA
}
# JWT Payload
payload = {
"iss": email, # Issuer claim
"sub": email, # Issuer claim
"aud": auth_url, # Audience claim
"iat": issued, # Issued At claim
"exp": expires, # Expire time
"scope": scope # Permissions
}
# Encode the headers and payload and sign creating a Signed JWT (JWS)
sig = jwt.encode(payload, pkey, algorithm="RS256", headers=additional_headers)
return sig
def exchangeJwtForAccessToken(signed_jwt):
'''
This function takes a Signed JWT and exchanges it for a Google OAuth Access Token
'''
auth_url = "https://www.googleapis.com/oauth2/v4/token"
params = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt
}
r = requests.post(auth_url, data=params)
if r.ok:
return(r.json()['access_token'], '')
return None, r.text
def gce_list_instances(accessToken):
'''
This functions lists the Google Compute Engine Instances in one zone
'''
# Endpoint that we will call
url = "https://www.googleapis.com/compute/v1/projects/" + project + "/zones/" + zone + "/instances"
# One of the headers is "Authorization: Bearer $TOKEN"
headers = {
"Host": "www.googleapis.com",
"Authorization": "Bearer " + accessToken,
"Content-Type": "application/json"
}
h = httplib2.Http()
resp, content = h.request(uri=url, method="GET", headers=headers)
status = int(resp.status)
if status < 200 or status >= 300:
print('Error: HTTP Request failed')
return
j = json.loads(content.decode('utf-8').replace('\n', ''))
print('Compute instances in zone', zone)
print('------------------------------------------------------------')
for item in j['items']:
print(item['name'])
if __name__ == '__main__':
cred = load_json_credentials(json_filename)
private_key = load_private_key(cred)
s_jwt = create_signed_jwt(
private_key,
cred['private_key_id'],
cred['client_email'],
scopes)
token, err = exchangeJwtForAccessToken(s_jwt)
if token is None:
print('Error:', err)
exit(1)
gce_list_instances(token)
For more information visit my blog. I write articles like this and publish the source code to help others understand how to write software for the cloud.
www.jhanley.com
NOTE: As noted in the comments, really this not a solution for the question because it uses SDK. Anyway, as the answers seems useful for other users, I've not deleted it
There is a simpler way to generate a token from a service account, using Google libraries
from google.auth.transport import requests
from google.oauth2 import service_account
CREDENTIAL_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
CREDENTIALS_KEY_PATH = '/PATH/TO/SERVICE_ACCOUNT.json'
def get_service_account_token():
credentials = service_account.Credentials.from_service_account_file(
CREDENTIALS_KEY_PATH, scopes=CREDENTIAL_SCOPES)
credentials.refresh(requests.Request())
return credentials.token
Or if you want to use the default authentication
import google
from google.auth.transport import requests
CREDENTIAL_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
def get_default_token():
credentials, project_id = google.auth.default(scopes=CREDENTIAL_SCOPES)
credentials.refresh(requests.Request())
return credentials.token
When credentials object is created, the token is empty, but after refreshing credentials, it contains the access token that can be used as header in the API requests
The same solution using JAVA
import com.google.auth.oauth2.GoogleCredentials;
import java.io.FileInputStream;
import java.io.IOException;
public class GoogleHelper {
public static String getAccessToken() throws IOException {
return GoogleCredentials
.fromStream(new FileInputStream("/PATH/TO/SERVICE_ACCOUNT.json"))
.createScoped("https://www.googleapis.com/auth/cloud-platform")
.refreshAccessToken()
.getTokenValue();
}
}

Ionic Google social authentication to Django Rest Framework backend

I am trying to get social authentication working for my mobile app (an Ionic app on Android). Django rest framework backend with rest_framework_jwt, social_django, and rest_social_auth.
On my Ionic app I was using satellizer.js, however, I can't use InAppBrowser so now I am trying to do the following with cordova-plugin-googleplus:
Step#1 (On client/app)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '[*.myclientid]', // optional clientId of your Web application from Credentials settings of your project - On Android, this MUST be included to get an idToken. On iOS, it is not required.
'offline': true, // optional, but requires the webClientId - if set to true the plugin will also return a serverAuthCode, which can be used to grant offline access to a non-Google server
}) ................
Result: This gets me a valid response with both a idToken, serverAuthCode, and a userId.
Step#2
I am not sure what the next step is. Originally, I was going to try using Django rest_social_auth to do the following from my client/app:
POST /api/login/social/
with data (json)
provider=google&code=ASLKDJASLDKJASLD
Which was supposed to return a JWT token (from my understanding of the docs), however, it is not passing the JWTAuthMixin as there is no value returned from a call to get_authorization_header(request).split() in that Mixin. These means that nothing is returned to my client/app except a 400 error.
Am I supposed to be adding a header to my Ionic app POST when passing my idToken or serverAuthCode? Or am I on the wrong side of the tracks...
Are there any implementation recommendations for this auth flow?
So far I did the following and it works.
1. On app/client
(The client uses satellizer.js and the cordova-plugin-googleplus)
if (provider == 'google') {
// Use Google API Natively to get tokens and user info
window.plugins.googleplus.login(
{
// TODO Get the WebClient from App settings
'webClientId': '*[googleclientid]*.apps.googleusercontent.com',
'offline': true
},
function (obj) {
$http.post(SERVER.url + '[MY BACKEND URL]' + '/google-oauth2/', {code: obj.idToken, servAuthCode: obj.serverAuthCode})
.success(function(data){
$auth.setToken(data.jwt_token);
/.. Do something ../
})
.error(function(data){
console.log("There was an error" + JSON.stringify(data));
});
},
function (msg) {
// TODO Set Error states
console.error('error: ' + msg);
}
);
}
Summary
The app calls the Google plus API googleplus.login method (sending my webClientId)
I post the resulting idToken and serverAuthCode obtained from google after login to my Django backend.
2. My backend methods
URL
My app/client hits the url(r'^[MY BACKEND URL]/(?P<backend>[\w-]+)/$', ObtainAuthToken.as_view(), ),
View
This calls the following view and functions:
class ObtainAuthToken(APIView):
permission_classes = (AllowAny,)
def post(self, request, backend):
data = request.data
user_tokenID = data['code']
server_auth_code = data['servAuthCode']
if user_tokenID and server_auth_code and verify_google_user_token_ID(user_tokenID):
# Get Google OAuth credentials for the verified GOOGLE user.
credentials = settings.GOOGLE_FLOW.step2_exchange(server_auth_code)
# Here we call PSA to authenticate like we would if we used PSA on server side.
user = register_by_access_token(request, backend, token=credentials.access_token)
# If user is active we get or create the REST token and send it back with user data
if user and user.is_active:
# Generate JWT token for user and pass back to client
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return JsonResponse({'id': user.id, 'name': user.username, 'jwt_token': token})
return JsonResponse({'status':'false','error':'Bad Credentials, check the Access Token and/or the UID'},
status=403)
def verify_google_user_token_ID(user_tokenID):
try:
google_http_request = google.auth.transport.requests.Request()
idinfo = verify_token(user_tokenID, request=google_http_request,
audience=settings.SOCIAL_AUTH_GOOGLE_OAUTH2_FULL_KEY)
# Or, if multiple clients access the backend server:
if idinfo['aud'] not in [settings.GOOGLE_APP_ID_ANDROID, settings.GOOGLE_APP_ID_WEB]:
raise crypt.AppIdentityError("Unrecognized client.")
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise crypt.AppIdentityError("Wrong issuer.")
return True
except crypt.AppIdentityError as e:
# Invalid token
return False
#psa('social:complete')
def register_by_access_token(request, backend, token):
backend = social_core.backends.google.GoogleOAuth2()
user = backend.do_auth(access_token=token, backend=backend)
if user:
return user
else:
return None
3. Back on the client
My client then looks at the response and takes the returned JWT and loads it to memory with $auth.setToken(data.jwt_token);
I think this works for now, but I still have to deal with token refresh and revocation etc.

Django allauth get credentials to make further requests on behalf of the user

I'm working on a Django project that requires user authentication for BitBucket, I have setup allauth such that users can authenticate with Bitbucket, I just don't understand how to make further requests to the Bitbucket API now that the user is authenticated.
I understand that allauth is purely for authentication purposes, there is just no documentation on how to access and make further use of the authentication, in this case accessing the credentials (oauth_token) such that I can make further requests on behalf of the resource-owner.
I found the authentication details to make a further requests.
Workflow
from allauth.socialaccount.models import SocialAccount, SocialApp
bitbucket_app = SocialApp.objects.get(provider='bitbucket')
user_account = SocialAccount.objects.get(user=request.user)
# User should only have one SocialToken object per SocialApp
# https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py#L137
user_token = useraccount.socialtoken_set.first()
# Credentials to make further requests
client_key = bitbucket_app.client_id
client_secret = bitbucket_app.secret
resource_owner_key = user_token.token
resource_owner_secret = user_token.token_secret
Using credentials with requests and requests_oauthlib
import requests
from requests_oathlib import OAuth1
auth = OAuth1(client_key, client_secret, resource_owner_key, resource_owner_secret)
r = requests.get(protected_url, auth=auth)
An example with the bitbucket-api
https://bitbucket-api.readthedocs.org/en/latest/index.html
from bitbucket.bitbucket import Bitbucket
bb = Bitbucket(user_account.uid) # Initialise with bitbucket username
bb.authorize(client_key, client_secret, 'http://localhost', resource_owner_key, resource_owner_secret)
# Get user repositories as an example
bb.repository.all()