Metadata-Version: 2.1
Name: py_mssso
Version: 0.0.1
Summary: MS SSO Helper for Python
Author-email: Sungwon Um <shineum@gmail.com>
License: The MIT License (MIT)
        
        Copyright (c) 2024 Sungwon Um
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
Project-URL: Homepage, https://github.com/shineum/py_mssso
Keywords: azure,sso,msal
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: msal>=1.31.0

# Installation

Use pip:

```
pip install py-mssso
```
or

```
pip install git+https://github.com/shineum/py_mssso.git
```


# Prerequisites
Create Azure App with MS Azure Protal "App registrations".
These information from Azure App are required.
- Directory (tenant) ID (Overview)
- Application (client) ID (Overview)
- Client secret (Manage - Certificates & secrets)
- scopes (Manage - API permissions)
- redirect url (Manage - Authentication - Web Redirect URIs)


# Getting Started
```
from py_mssso import MSSSOHelper

...

MSSSOHelper.add(
    **{
        "tenant_type": MS_SSO_TENANT_TYPE,
        "tenant_id": MS_SSO_TENANT_ID,
        "client_id": MS_SSO_CLIENT_ID,
        "client_secret": MS_SSO_CLIENT_SECRET,
        "scopes": MS_SSO_SCOPES,
        "redirect_url": MS_SSO_REDIRECT_URL,
    }
)
```

#### tenant_type
```
"SINGLE", "MULTI" or "PUBLIC" - default: "SINGLE"
```

#### scopes
```
["User.Read"]: Read user data only for login user
["User.Read.All"]: when you need to read other user's information (ex: manager of login user)
```

#### redirect_url
```
url must be set in Web Redirect URIs in Azure App
```


# login url
django example
```
# view.py
from py_mssso import MSSSOHelper
from django.http import HttpResponseRedirect

...

# sso login
def sso_login(request):
    if not request.session.session_key:
        request.session.create()
    msal_flow = MSSSOHelper.get().get_auth_code_flow()
    request.session["msal_flow"] = msal_flow
    request.session.save()
    return HttpResponseRedirect(msal_flow.get("auth_uri"))
```


# callback url
django example
```
# view.py
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth import login
from django.http import HttpResponseRedirect
from py_mssso import MSSSOHelper

_LOGIN_FAIL_URL = "/"
_LOGIN_SUCCESS_URL = "/"
_MSGRAPH_BASE_URI = "https://graph.microsoft.com/v1.0"
_MSGRAPH_QUERY_SELECT_ITEMS_FOR_USER = ",".join(
    [
        "displayName",
        "givenName",
        "surname",
        "mail",
        "mobilePhone",
        "officeLocation",
        "userPrincipalName",
        "jobTitle",
        "department",
        "companyName",
        "onPremisesSamAccountName",
    ]
)

...

# django login helper
def _login(request, username):
    try:
        user = get_user_model().objects.get(username=username)
        user.backend = "django.contrib.auth.backends.ModelBackend"
        login(request, user)
    except:
        raise Exception(f"Login Failed - user not exist: [{username}]")

# sso callback
def sso_login_callback(request):
    code = request.GET.get("code")
    state = request.GET.get("state")
    session_state = request.GET.get("session_state")
    msal_flow = request.session.get("msal_flow")

    try:
        # request validation
        if not code:
            raise Exception("Invalid request")

        # get token info
        token_info = MSSSOHelper.get().get_token_info(
            auth_code_flow=msal_flow,
            auth_res={
                "code": code,
                "state": state,
                "session_state": session_state,
            },
        )

        # token info validation
        if not token_info:
            raise Exception("Invalid request")

        # find token
        token = token_info.get("access_token")

        # token validation
        if not token:
            raise Exception("Invalid request")

        # set request header
        headers = {"Authorization": f"Bearer {token}"}

        # get user data with MS Graph API
        res_user = requests.get(
            f"{_MSGRAPH_BASE_URI}/me?$select={_MSGRAPH_QUERY_SELECT_ITEMS_FOR_USER}",
            headers=headers,
        )

        # get manager data with MS Graph API (Optional, "User.Read.All" permission is required)
        res_manager = requests.get(
            f"{_MSGRAPH_BASE_URI}/me/manager?$select={_MSGRAPH_QUERY_SELECT_ITEMS_FOR_USER}",
            headers=headers,
        )

        ##############################################
        # Run some processes with user / manager data
        # ex) create (django) user if not exist
        #     or update profile if necessary
        ##############################################

        # parse user object
        ms_user_obj = res_user.json()

        # set username - it could be "userPrincipalName", "onPremisesSamAccountName" or any other values depending on your settings
        username = ms_user_obj.get("userPrincipalName")
        if not username:
            raise Exception("Invalid User")

        # create or update django user
        get_user_model().objects.update_or_create(
            username=username,
            defaults={
                "first_name": ms_user_obj.get("givenName"),
                "last_name": ms_user_obj.get("surname"),
                "email": ms_user_obj.get("mail"),
                "is_active": True,
            },
        )

        # django login
        _login(request, username)
    except:
        return HttpResponseRedirect(_LOGIN_FAIL_URL)

    return HttpResponseRedirect(_LOGIN_SUCCESS_URL)
```

# sample project
```
https://github.com/shineum/py_mssso_sample
```
