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

feat: Flexible fields mapping for AAD user info and FAB user #1912

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

passren
Copy link

@passren passren commented Aug 18, 2022

Description

Currently in security module for AAD OAuth, if get user info from AAD, the module will return a fixed values which map user info from AAD to fab user as below:

return {
    "name": me.get("name", ""),
    "email": me["upn"],
    "first_name": me.get("given_name", ""),
    "last_name": me.get("family_name", ""),
    "id": me["oid"],
    "username": me["oid"],
    "role_keys": me.get("roles", []),
}

But I found some of fields from AAD may be missed like "upn" can't retrieve from AAD, and the fixed mapping doesn't meet my requirements, like "email" should map to me["email"] in my scenario.
This PR is to enable the fields mapping can be set in config.py for more flexible configuration.

The new mapping structure is blow:

"user_info_mapping": {
    "name": ("name", ""),
    "email": ("email", ""),
    "first_name": ("given_name", ""),
    "last_name": ("family_name", ""),
    "id": ("oid", ""),
    "username": ("preferred_username", ""),
    "role_keys": ("roles", []),
}

The field mapping shall look like this
"<FAB user info field>": ("<user info field from AAD>", <default value if no this field from AAD>).

The fields mapping will be put into this path: OAUTH_PROVIDERS > {provider: azure} > "remote_app" > "client_kwargs"

OAUTH_PROVIDERS = [
{
        "name": "azure",
        "icon": "fa-windows",
        "token_key": "access_token",
        "remote_app": {
            "client_id": "AZURE_APPLICATION_ID",
            "client_secret": "AZURE_SECRET",
            "api_base_url": "https://login.microsoftonline.com/AZURE_TENANT_ID/oauth2",
            "client_kwargs": {
                "scope": "User.read name preferred_username email profile upn",
                "resource": "AZURE_APPLICATION_ID",
                "user_info_mapping": {
                    "name": ("name", ""),
                    "email": ("email", ""),
                    "first_name": ("given_name", ""),
                    "last_name": ("family_name", ""),
                    "id": ("oid", ""),
                    "username": ("preferred_username", ""),
                    "role_keys": ("roles", []),
                }
            },
            "request_token_url": None,
            "access_token_url": "https://login.microsoftonline.com/AZURE_TENANT_ID/oauth2/token",
            "authorize_url": "https://login.microsoftonline.com/AZURE_TENANT_ID/oauth2/authorize",
        },
    },
]

ADDITIONAL INFORMATION

@passren passren changed the title Flexible fields mapping for AAD user info and FAB user feat: Flexible fields mapping for AAD user info and FAB user Aug 18, 2022
@krionbsd
Copy link

@passren Could you please fix black formatting to get it like that:

--- a/flask_appbuilder/security/manager.py
+++ b/flask_appbuilder/security/manager.py
@@ -615,12 +615,16 @@ class BaseSecurityManager(AbstractSecurityManager):
             log.debug(str(id_token))
             me = self._azure_jwt_token_parse(id_token)
             log.debug("Parse JWT token : {0}".format(me))
-            user_info_map = self.oauth_remotes[provider].client_kwargs.get("user_info_mapping")
+            user_info_map = self.oauth_remotes[provider].client_kwargs.get(
+                "user_info_mapping"
+            )

             user_info_data = {}
             if user_info_map:
                 for user_info_field, aad_user_info_field in user_info_map.items():
-                    user_info_data[user_info_field] = me.get(aad_user_info_field[0], aad_user_info_field[1])
+                    user_info_data[user_info_field] = me.get(
+                        aad_user_info_field[0], aad_user_info_field[1]
+                    )
             else:
                 user_info_data = {
                     "name": me.get("name", ""),

@krionbsd
Copy link

@passren I tried above patch and new mapping with "email": ("email", "") but it doesn't seem to solve the issue, still getting ERROR - Error returning OAuth user info: 'upn' error

@passren
Copy link
Author

passren commented Sep 1, 2022

@krionbsd just remove "upn" from client_kwargs > scope, like this:

"client_kwargs": {
"scope": "User.read name preferred_username email profile",

@krionbsd
Copy link

krionbsd commented Sep 1, 2022

User.read name preferred_username email profile

I don't have upn in client_kwargs scope, but instead 'scope': 'openid profile email groups',

@passren
Copy link
Author

passren commented Sep 2, 2022

User.read name preferred_username email profile

I don't have upn in client_kwargs scope, but instead 'scope': 'openid profile email groups',

That's strange. Please check if "user_info_mapping" was placed under client_kwargs. Only if the code can't get mapping from config file, it will try to get default "upn" from AAD user response.

@georgewfisher
Copy link

@passren I have tested your change with Superset and found that it works and addresses the claim mapping issue.

@dpgaspar can we get this change checked in?

@krionbsd It's possible your config was missing something, or you were missing the passren's change. Here's what I used:

OAUTH_PROVIDERS = [
        {
            "name": "azure",
            "icon": "fa-windows",
            "token_key": "access_token",
            "remote_app": {
               "client_id": "{{CLIENT_ID}}",
               "client_secret": os.environ.get("AZURE_SECRET"),
               "api_base_url": "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/",
               "client_kwargs": {
                  "scope": "User.read email profile openid",
                  "resource": "{{CLIENT_ID}}",
                  "user_info_mapping":
                  {
                    "name": ("name", ""),
                    "email": ("email", ""),
                    "first_name": ("given_name", ""),
                    "last_name": ("family_name", ""),
                    "id": ("oid", ""),
                    "username": ("preferred_username", ""),
                    "role_keys": ("roles", []),
                  }
               },
               "request_token_url": None,
               "access_token_url": "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token",
               "authorize_url": "https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize",
               "jwks_uri": "https://login.microsoftonline.com/common/discovery/v2.0/keys"
           }
       }
    ]

@ms-uhrs
Copy link

ms-uhrs commented Oct 19, 2022

@passren I have test this on AKS, it works for me too. Thank you for your proposed solution and we hope this PR could be merged

@dpgaspar This issue is a common issue for azure aad users, another team from my part also have a similar one. Could you please help to merge the pr?

Copy link

@ms-uhrs ms-uhrs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good for me. I have try it in my projects and succeeded.

@krionbsd
Copy link

I've been using dex as OpenID provider which drives authentication to Azure, for whatever reasons I'm still getting the error with above mentioned config as well as with @passren patch:

ERROR - Error authorizing OAuth access token: invalid_scope: Unrecognized scope(s) ["User.read"]

@passren
Copy link
Author

passren commented Oct 21, 2022

@georgewfisher If you are trying to config AAD in Superset, you could refer to this: Config Superset > Custom OAuth2 Configuration.
Write a CustomSsoSecurityManager to resolve FAB AAD issue. Here is my code:

import logging
from superset.security import SupersetSecurityManager

class AADSecurityManager(SupersetSecurityManager):

    def oauth_user_info(self, provider, response=None):
        logging.debug("Oauth2 provider: {0}.".format(provider))
        if provider == 'azure':
            logging.debug("Azure response received : {0}".format(response))
            id_token = response["id_token"]
            logging.debug(str(id_token))
            me = self._azure_jwt_token_parse(id_token)
            logging.debug("Parse JWT token : {0}".format(me))
            return {
                "name": me.get("name", ""),
                "email": me["email"],
                "first_name": me.get("given_name", ""),
                "last_name": me.get("family_name", ""),
                "id": me["oid"],
                "username": me["preferred_username"],
                "role_keys": me.get("roles", []),
            }

Then set this manager class in config file, like this:

from custom_sso_security_manager import AADSecurityManager
CUSTOM_SECURITY_MANAGER = AADSecurityManager

Copy link

@georgewfisher georgewfisher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this and it works.

@Narender-007
Copy link

@georgewfisher If you are trying to config AAD in Superset, you could refer to this: Config Superset > Custom OAuth2 Configuration. Write a CustomSsoSecurityManager to resolve FAB AAD issue. Here is my code:

import logging
from superset.security import SupersetSecurityManager

class AADSecurityManager(SupersetSecurityManager):

    def oauth_user_info(self, provider, response=None):
        logging.debug("Oauth2 provider: {0}.".format(provider))
        if provider == 'azure':
            logging.debug("Azure response received : {0}".format(response))
            id_token = response["id_token"]
            logging.debug(str(id_token))
            me = self._azure_jwt_token_parse(id_token)
            logging.debug("Parse JWT token : {0}".format(me))
            return {
                "name": me.get("name", ""),
                "email": me["email"],
                "first_name": me.get("given_name", ""),
                "last_name": me.get("family_name", ""),
                "id": me["oid"],
                "username": me["preferred_username"],
                "role_keys": me.get("roles", []),
            }

Then set this manager class in config file, like this:

from custom_sso_security_manager import AADSecurityManager
CUSTOM_SECURITY_MANAGER = AADSecurityManager

but this is not correct procedure right because when we are deploying production how can we edit manager clients are not acceptable that way please give me better solution

Thanks..

@Firxiao
Copy link

Firxiao commented Jun 26, 2024

May I know why this PR is still being pending? Thank u all your guys work.

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

Successfully merging this pull request may close these issues.

6 participants