Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration with OAuth provider okta #13948

Closed
vinit2580 opened this issue Apr 5, 2021 · 30 comments
Closed

Integration with OAuth provider okta #13948

vinit2580 opened this issue Apr 5, 2021 · 30 comments

Comments

@vinit2580
Copy link

Hi,
I am trying to integrate okta using OAuth but everytime it gives me invalid login. Please try again message.
My superset_config.py has below configuration:

import os
from flask import Flask

import logging
from flask_appbuilder.security.manager import AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH
from superset.security import SupersetSecurityManager
import logging
from flask_appbuilder import SQLA, AppBuilder

class CustomSsoSecurityManager(SupersetSecurityManager):

def oauth_user_info(self, provider, response=None):
    logging.info("Oauth2 provider: {0}.".format(provider))
    if provider == 'okta':
        # 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
        res = self.appbuilder.sm.oauth_remotes[provider].get('https://dev-514411.okta.com/oauth2/default/v1/userinfo')
        logging.info(" {0}".format(res))
        if res.status != 200:
            logger.error('Failed to obtain user info: %s', res.data)
            return
        logging.info("user_data: {0}".format(res))
        return {'name': res['firstName'], 'email': res['email'], 'id': res['login'], 'username': res['login'],
                'first_name': '', 'last_name': ''}
    #  return {'name': 'neeraj', 'email': '[email protected]', 'id': '[email protected]', 'username': '[email protected]',
    #         'first_name': '', 'last_name': ''}

Superset specific config

ROW_LIMIT = 5000
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = 'Admin'
AUTH_ROLE_ADMIN = 'Admin'
AUTH_ROLE_PUBLIC = 'Admin'
WTF_CSRF_EXEMPT_LIST = ['']

Flask App Builder configuration

Your App secret key

SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'

AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [{
'name': 'okta',
'token_key': 'access_token', # Name of the token in the response of access_token_url
'icon':'fa-circle-o', # Icon for the provider
'remote_app': {
'client_id': '0oa8hoe9t1c8555666091z357', # Client Id (Identify Superset application)
'client_secret': 'b8exxJID0BQOXlvMl1234565frU4OY7FX3cXDOMLM', # Secret for this Client Id (Identify Superset application)
'client_kwargs': {
'scope': 'openid'
},
'access_token_method': 'POST', # HTTP Method to call access_token_url
'access_token_headers': { # Additional headers for calls to access_token_url
'Authorization': 'Basic MG9hOGhvZTl0MWM4THhCMXozNTc6YjhleHhKSUQwQlFPWGx2TWxRYTVUbzVmclU0T1k3RlgzY1hET01MTQ=='
},
'base_url': 'https://dev-514411.okta.com/oauth2/default/',
'authorize_url': 'https://dev-514411.okta.com/oauth2/default/v1/authorize',
'access_token_url': 'https://dev-514411.okta.com/oauth2/default/v1/token',
'redirect_uris': ['http://127.0.0.1:8088/oauth-authorized/okta']
}
}]

CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager

Whenever i try to login. It gives below error message :
image

image

I got stuck here. i followed the steps mentioned into superset configuration settings. Can someone help me here please ?

@dpgaspar
Copy link
Member

dpgaspar commented Apr 5, 2021

@vinit2580,

Note that OKTA will be supported out of the box on FAB on 3.2.2 to be release this week. So you don't have to write your own custom security class

@vinit2580
Copy link
Author

@dpgaspar - Now i changed my superset_config.py file and have below configuration only.
AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [
{'name': 'okta', 'icon': 'fa-circle-o',
'token_key': 'access_token',
'remote_app': {
'client_id': '0oa8hoe9t1c8LfB1z357',
'client_secret': 'b8exxJID0BQOXlvMlQa5To5frU4EY7FX3cXDOMLM',
'api_base_url': 'https://dev-514411.okta.com/oauth2/v1/',
'client_kwargs': {
'scope': 'openid profile email groups'
},
'access_token_url': 'https://dev-514411.okta.com/oauth2/v1/token',
'authorize_url': 'https://dev-514411.okta.com/oauth2/v1/authorize'
}
}
]

It allows me to authenticate using okta but after authentication. It redirects me to login page with message 'Invalid login. Please try again.
There is no error log on console.
image

Thanks in advance
Vinit

@mrshu
Copy link
Contributor

mrshu commented Apr 20, 2021

@dpgaspar would you happen to have any updates on FAB's version in Airflow? Was it updated to 3.2.2 by any chance in the 2.0.2 release?

@formerdev
Copy link

I have a similar scenario. But when I select Okta as a provider and click the Sign In button I'm redirected to an error page that says:

400 - Bad Request
Description: The 'redirect_uri' parameter must be a Login redirect URI in the client app settings

I tried using http://localhost:8088/ and http://localhost:8088/login/ as sign-in redirect uri's, but to no avail. Any ideas on what to use as the sign-in redirect uri?

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 7, 2021

@Bonifacio-Oliveira have you tried using https://<superset-fqdn>/oauth-authorized/okta? you should be able to see it was presented in the request to the configured authorize URL like this:

https://dev-514411.okta.com/oauth2/v1/authorize?response_type=code&scope=openid+profile+email+groups&client_id=0oa8hoe9t1c8LfB1z357&redirect_uri=https%3A%2F%2F<your-superset-fqdn>%2Foauth-authorized%2Fokta

See #15010

@formerdev
Copy link

@shawnzhu It worked. Thank you for the response and for the doc update. :)

@formerdev
Copy link

Hello again, @shawnzhu . I reread the entire discussion here and noticed that the version I'm running locally was supposed to already have a built-in Okta support. But when I try to run the app without a CustomSsoSecurityManager I get the error message described in this issue's first message.

It's nice that we were able to improve the docs on the redirect uri usage, but unfortunately we haven't solved the original issue yet.

Any suggestions on what to try? Please let me know if you need additional information.

@formerdev
Copy link

In my case, the server side has the following logs:

2021-06-07 20:13:57,993:DEBUG:authlib.integrations.base_client.base_app:Saving authorize data: {'redirect_uri': 'http://localhost:8088/oauth-authorized/okta', 'nonce': '---', 'url': 'https://dev-428334.okta.com/oauth2/v1/authorize?response_type=code&client_id=<a-client-id>&redirect_uri=http%3A%2F%2Flocalhost%3A8088%2Foauth-authorized%2Fokta&scope=openid+profile+email+groups&state=<a-jwt>&nonce=<a-nonce>', 'state': '<a-jwt>'}
172.18.0.1 - - [07/Jun/2021:20:13:57 +0000] "GET /login/okta?next= HTTP/1.1" 302 913 "http://localhost:8088/login/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
2021-06-07 20:13:58,973:DEBUG:authlib.integrations.base_client.base_app:Retrieve temporary data: {'code': '---', 'state': '<a-jwt>', 'redirect_uri': 'http://localhost:8088/oauth-authorized/okta'}
2021-06-07 20:13:58,975:DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): dev-428334.okta.com:443
2021-06-07 20:14:00,151:DEBUG:urllib3.connectionpool:https://dev-428334.okta.com:443 "POST /oauth2/v1/token HTTP/1.1" 200 None
2021-06-07 20:14:00,154:INFO:root:Oauth2 provider: okta.
2021-06-07 20:14:00,156:DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): dev-428334.okta.com:443
2021-06-07 20:14:01,026:DEBUG:urllib3.connectionpool:https://dev-428334.okta.com:443 "GET /oauth2/v1/userDetails HTTP/1.1" 405 None
2021-06-07 20:14:01,029:ERROR:flask_appbuilder.security.views:Error returning OAuth user info: 'Response' object has no attribute 'data'
172.18.0.1 - - [07/Jun/2021:20:14:01 +0000] "GET /oauth-authorized/okta?code=zwHoTZeVMl4qlAZ7NPVbTPp-dxnSdzX3kQUyKsyY0z0&state=<a-jwt> HTTP/1.1" 302 221 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
172.18.0.1 - - [07/Jun/2021:20:14:01 +0000] "GET /login/ HTTP/1.1" 200 24440 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"

@nytai
Copy link
Member

nytai commented Jun 7, 2021

@Bonifacio-Oliveira looks like we're getting a 405 from okta /oauth2/v1/userDetails HTTP/1.1" 405. Are you sure that's the correct endpoint for fetching user info?

@nytai
Copy link
Member

nytai commented Jun 7, 2021

the okta docs say it's /userinfo
https://developer.okta.com/docs/reference/api/oidc/#userinfo

@nytai
Copy link
Member

nytai commented Jun 7, 2021

@vinit2580 The error in your logs is coming from the code in your custom security manager

if res.status != 200:

python Response objects don't have that attribute, I think you're looking for status_code instead

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

Found this gist titled Enable Okta Login for Superset: https://gist.github.com/ktmud/2475282a166893e5d17039c308cbe50d

my working setup

I get used to specify server_metadata_url with the discovery URL of given oidc provider so that I don't need to manually specify userinfo_endpoint together with other properties like authorize_url.

After configuring either server_metadata_url or userinfo_endpoint, it should be able to parse id token directly (at least with the openid connect provider I use):

class CustomSecurityManager(SupersetSecurityManager):
    '''
    Custom security manager to support my OpenID Connect
    '''

    def oauth_user_info(self, provider, response=None):
        if provider == 'my-oidc-provider-name':
            # As OpenID connect 1.0 provider, it provides id_token in response
            user_info = self.appbuilder.sm.oauth_remotes[provider].parse_id_token(response)
            return {
                # use email as username
                'username': user_info['email'],
                'email': user_info['email']
            }

let me know if it works for you or not

@formerdev
Copy link

Hello, @shawnzhu . I tried your code and it didn't work. Let me share mine:

class CustomSsoSecurityManager(SupersetSecurityManager):

    def oauth_user_info(self, provider, response=None):
        if provider == 'okta':
            user_info = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')

            me = user_info.json()

            return {
                'name': me['name'],
                'email': me['email'],
                'id': me['email'],
                'username': me['email'],
                'first_name': me['given_name'],
                'last_name': me['family_name']
            }

I'm now able to retrieve the data from Okta, but it's still not clear to me what is the interface of the object oauth_user_info is supposed to return.

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

@Bonifacio-Oliveira I had the similar experience, do you mind adding debug info via logging?

in your CustomSsoSecurityManager:

import logging
logger = logging.getLogger(__name__)

logger.setLevel(logging.DEBUG)

class CustomSsoSecurityManager(SupersetSecurityManager):
    def oauth_user_info(self, provider, response=None):
        log.debug('oauth2 provider: {0}'.format(provider))
        log.debug('response: {0}'.format(response))
        if provider == 'okta':
            user_info = self.appbuilder.sm.oauth_remotes[provider].get('userinfo')
            log.debug('user_info: {0}'.format(user_info))
            # maybe you want to debug the parsed id_token since you said it doesn't work for you
            # user_info = self.appbuilder.sm.oauth_remotes[provider].parse_id_token(response)

            me = user_info.json()

            return {
                'name': me['name'],
                'email': me['email'],
                'id': me['email'],
                'username': me['email'],
                'first_name': me['given_name'],
                'last_name': me['family_name']
            }

Question: have you enabled the userinfo_endpoint or server_metadata_url in the oauth provider config in superset_config.py?

@formerdev
Copy link

Here are the logs you asked:

2021-06-08 14:26:58,921:DEBUG:custom_sso_security_manager:oauth2 provider: okta
2021-06-08 14:26:58,922:DEBUG:custom_sso_security_manager:response: {'token_type': 'Bearer', 'expires_in': 3600, 'access_token': '<a-token>', 'scope': 'openid profile email groups', 'id_token': '<a-token>', 'expires_at': 1623166018}
2021-06-08 14:26:59,996:DEBUG:custom_sso_security_manager:user_info: <Response [200]>

About your question: No, I haven't. Is this required? If so, how do I do this? Any documentation on the subject?

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

@Bonifacio-Oliveira So your debug info showed that it has provided an id token in the response object, so do you mind uncommenting the line that parse id_token:

user_info = self.appbuilder.sm.oauth_remotes[provider].parse_id_token(response)
log.debug('user_info: {0}'.format(user_info))

Then you can see if you can use the user_info to construct the required return value

how do I do this? Any documentation on the subject?

The doc is here: https://github.com/lepture/authlib/blob/33aab0272d7c8a857c851f49a32c6d374930549a/authlib/integrations/base_client/base_app.py#L14-L55

@formerdev
Copy link

Uncommenting the line you mentioned results in an error:

2021-06-08 16:11:09,210:ERROR:flask_appbuilder.security.views:Error returning OAuth user info: Missing "jwks_uri" in metadata

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

According to Okta, it needs to retrieve the jwks_uri to verify the signature of an id token. see https://developer.okta.com/docs/guides/validate-id-tokens/overview/#retrieve-the-json-web-key-set

PS: you could specify the /.well-known/openid-configuration as server_metadata_url so that you don't need to specify all other metadata one by one.

I really should document this in the official doc as well.

@formerdev
Copy link

Set /.well-known/openid-configuration as server_metadata_url and now parse_id_token() works. But I still get the Invalid login. Please try again. error message.

How do I know what I have to return when I override oauth_user_info? I'm returning an object like this, but it doesn't work:

{
    'name': user_info['name'],
    'email': user_info['email'],
    'id': user_info['email'],
    'username': user_info['email'],
}

@formerdev
Copy link

And hopefully one of the outcomes of this discussion is better documentation on how to set things up. I'm happy to help with what I can. :)

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

How do I know what I have to return when I override oauth_user_info? I'm returning an object like this, but it doesn't work:

This is the logic of the latest flask-appbuilder:

this is how flask appbuilder uses user_info: https://github.com/dpgaspar/Flask-AppBuilder/blob/dae4dd47d51e1e2eb5894bce55221c1d26864c3b/flask_appbuilder/security/manager.py#L1287-L1302

So the key attributes are username and email (if username is not available), and you must have that user account in superset db with a matching username (in your code, a user's username must be the case sensitive email). As long as you have a matching record from the table ab_user, it should login that user in.

@nytai
Copy link
Member

nytai commented Jun 8, 2021

Ah, so the user isn't upserted, it's just matched against an existing user in the db based on the username attribute.

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 8, 2021

it should be an upsert if AUTH_USER_REGISTRATION = True. see https://github.com/dpgaspar/Flask-AppBuilder/blob/dae4dd47d51e1e2eb5894bce55221c1d26864c3b/flask_appbuilder/security/manager.py#L1322-L1330

@formerdev
Copy link

I was finally able to login successfully. I just searched the repo and didn't find any reference to ab_user on the docs. I don't know how I was supposed to figure this out on my own. Thank you very much for your help, @shawnzhu

@shawnzhu
Copy link
Contributor

shawnzhu commented Jun 9, 2021

Glad to hear it works for you!

Sorry I didn't make it clear. The ab_user is the database table name of the Flask AppBuilder user model:

https://github.com/dpgaspar/Flask-AppBuilder/blob/27b15e59316e85e0fe62b8aa9978391ed4c729c9/flask_appbuilder/security/sqla/models.py#L94-L108

As a superset administrator, you can run command superset fab list-users with superset_config.py to see the list of users.

@nikhil-kuyya-talentas
Copy link

can share insights to logout side of code. thank you.

@mdeshmu
Copy link
Contributor

mdeshmu commented Apr 21, 2022

can someone please share their working code here.

@nikhil-kuyya-talentas
Copy link

nikhil-kuyya-talentas commented Apr 21, 2022

from flask_appbuilder.security.views import AuthOAuthView
from flask_appbuilder import expose
from flask import redirect, session
from superset.security import SupersetSecurityManager
import logging

class CustomSsoAuthOAuthView(AuthOAuthView):

  @expose("/logout/")
  def logout(self, provider="okta", register=None):
      provider_obj = self.appbuilder.sm.oauth_remotes[provider]
      app = self.appbuilder.get_app
      postLogoutRedirectURI = app.config.get("POST_LOGOUT_REDIRECT_URI")
      logout_url = ("{}?id_token_hint={}&post_logout_redirect_uri={}".format(
          provider_obj.server_metadata.get('end_session_endpoint'),
          session.get('id_token'),
          postLogoutRedirectURI
      ))
      ret = super().logout()
      return redirect(logout_url)

class CustomSsoSecurityManager(SupersetSecurityManager):
authoauthview = CustomSsoAuthOAuthView

  def __init__(self, appbuilder):
      super(CustomSsoSecurityManager, self).__init__(appbuilder)

  def get_oauth_user_info(self, provider, resp):

      if provider == "okta":
          res = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
          if res.status_code == 200:
              me = res.json()
              session['id_token'] = resp['id_token']
              logging.debug("User info from Okta: {0}".format(me))
              return {
                  "username": "okta_" + me.get("name", ""),
                  "first_name": me.get("given_name", ""),
                  "last_name": me.get("family_name", ""),
                  "email": me.get("email", ""),
              }
          else:
              return {}

@nikhil-kuyya-talentas
Copy link

@mdeshmu

  1. add Authlib==0.15.5 as dependency
  2. configure the okta superset in config
  3. add the custom code to authentication aoove coment mentioned.

i did some customization for logout to get the url from config.

@nikhil-kuyya-talentas
Copy link

I see that as per suggestion from FAB already has okta setup.
May be i need to understand writing the superset with out custom classes.
find the link for the discussion for above point:
https://apache-superset.slack.com/archives/C015WAZL0KH/p1650537627466969?thread_ts=1650535553.697419&cid=C015WAZL0KH

Just putting if some find it's useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants