Azure B2C : Error in the callback after the edit profile when it tries to get a new token
I try to implement an azure B2C authentication in Django.
Unfortunately, there is not much documentation in Django on this topic. However, I managed to write the functions and views to get an id_token and store the user's information in the session.
I wanted to integrate the edit profile with the specific authority after that the user is authenticated. The page is perfectly redirected to update the user information. However, after the validation of the new data, I get an error message in the callback when I try to get a new token (function _get_token_from_code
).
{'error': 'invalid_grant', 'error_description': 'AADB2C90088: The provided grant has not been issued for this endpoint. Actual Value : B2C_1_signupsignin_20220810 and Expected Value : B2C_1_profileediting\r\nCorrelation ID: xxxxxxxxxxx nTimestamp: 2022-08-31 14:58:54Z\r\n'}
So it implies the following error in the python execution :
_store_user(request, result['id_token_claims'])
KeyError: 'id_token_claims'
However the new information of the user are correctly saved in the azure.
Since I am newbee in this authentication process ? Do we need to generate a new token for the edit profile user flow ? Where does come from this mistake ?
Here is the code in djnago :
load_dotenv()
def initialize_context(request):
context = {}
# Check for any errors in the session
error = request.session.pop('flash_error', None)
if error != None:
context['errors'] = []
context['errors'].append(error)
# Check for user in the session
context['user'] = request.session.get('user', {'is_authenticated': False})
return context
def index(request) :
context = initialize_context(request)
return render(request, 'index.html', context)
def sign_in(request) :
flow = _build_auth_code_flow()
try:
request.session['auth_flow'] = flow
except Exception as e:
print(e)
return HttpResponseRedirect(flow['auth_uri'])
def callback(request) :
result = _get_token_from_code(request)
print(result)
# Store user from auth_helper.py script
_store_user(request, result['id_token_claims'])
return redirect('home')
def home(request) :
context = initialize_context(request)
context['edit'] = os.getenv("B2C_PROFILE_AUTHORITY")
context['user'] = request.session['user'].get('emails')[0]
return render(request, 'home.html', context)
def editprofile(request) :
authority = os.getenv("B2C_PROFILE_AUTHORITY")
flow = _build_auth_code_flow(authority=authority)
try:
request.session['auth_flow'] = flow
except Exception as e:
print(e)
return HttpResponseRedirect(flow['auth_uri'])
#----- Library -----------------------
def _build_msal_app(cache=None, authority=None):
auth_app = msal.ConfidentialClientApplication(
os.getenv("CLIENT_ID"),
authority=authority or os.getenv("AUTHORITY"),
client_credential=os.getenv("CLIENT_SECRET"),
token_cache=cache)
return auth_app
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
redirect_uri="http://localhost:8000/getAToken")
def _get_token_from_code(request):
cache = _load_cache(request)
auth_app = _build_msal_app(cache)
# Get the flow saved in session
flow = request.session.pop('auth_flow', {})
result = auth_app.acquire_token_by_auth_code_flow(flow, request.GET)
_save_cache(request, cache)
return result
def _load_cache(request):
# Check for a token cache in the session
cache = msal.SerializableTokenCache()
if request.session.get('token_cache'):
cache.deserialize(request.session['token_cache'])
return cache
def _save_cache(request, cache):
# If cache has changed, persist back to session
if cache.has_state_changed:
request.session['token_cache'] = cache.serialize()
def _store_user(request, user):
try:
request.session['user'] = {
'is_authenticated': True,
'name': user['given_name'],
'emails': user['emails'] if (user['emails'] != None) else user['userPrincipalName'],
# 'timeZone': user['mailboxSettings']['timeZone'] if (user['mailboxSettings']['timeZone'] != None) else 'UTC'
}
except Exception as e:
print(e)
This is a solution using https://django-allauth.readthedocs.io/en/latest/ We are using this library in several projects for authentication with MS Azure/Graph and other providers. It provides adapters for a LOT of identity provider platforms.
- add to INSTALLED_APPS:
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.microsoft',
- your django-allauth settings need to include:
SOCIALACCOUNT_PROVIDERS = {
'microsoft': {
'tenant': 'XXX', # replace this with your tenant name, see Azure
'SCOPE': ['User.Read', 'openid', 'email', 'profile'],
}
}
- as you probably have custom login forms, you need to add the login functionality from django-allauth to them. This is an example, see the django-allauth documentation for explanation.
{% load i18n static socialaccount %}
{% for provider in socialaccount_providers %}
<p>
<a href="{% provider_login_url provider.id process='login' scope=scope auth_params=auth_params %}">
{% trans "Log in with" %}
<strong>{{ provider.name}}</strong>
</a>
</p>
{% endfor %}
django-allauth will store tokens in its own models. You can access tokens for a user like this:
from allauth.socialaccount.models import SocialToken
social_token = SocialToken.objects.filter(
account__user=request.user, account__provider='microsoft'
).order_by('-expires_at').first()
if social_token and social_token.token:
# social_token.token is the OAuth2 access_token